PEP 678: Enriching Exceptions with Notes (#2201)
Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
This commit is contained in:
parent
886624a3a5
commit
8c43285a06
|
@ -533,6 +533,7 @@ pep-0674.rst @vstinner
|
|||
pep-0675.rst @jellezijlstra
|
||||
pep-0676.rst @Mariatta
|
||||
pep-0677.rst @gvanrossum
|
||||
pep-0678.rst @iritkatriel
|
||||
# ...
|
||||
# pep-0754.txt
|
||||
# ...
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
PEP: 678
|
||||
Title: Enriching Exceptions with Notes
|
||||
Author: Zac Hatfield-Dodds <zac@zhd.dev>
|
||||
Sponsor: Irit Katriel
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Content-Type: text/x-rst
|
||||
Requires: 654
|
||||
Created: 20-Dec-2021
|
||||
Python-Version: 3.11
|
||||
Post-History:
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
Exception objects are typically initialized with a message that describes the
|
||||
error which has occurred. Because further information may be available when the
|
||||
exception is caught and re-raised, this PEP proposes to add a ``.__note__``
|
||||
attribute and update the builtin traceback formatting code to include it in
|
||||
the formatted traceback following the exception string.
|
||||
|
||||
This is particularly useful in relation to :pep:`654` ``ExceptionGroup`` s, which
|
||||
make previous workarounds ineffective or confusing. Use cases have been identified
|
||||
in the standard library, Hypothesis package, and common code patterns with retries.
|
||||
|
||||
|
||||
Motivation
|
||||
==========
|
||||
When an exception is created in order to be raised, it is usually initialized
|
||||
with information that describes the error that has occurred. There are cases
|
||||
where it is useful to add information after the exception was caught.
|
||||
For example,
|
||||
|
||||
- testing libraries may wish to show the values involved in a failing assertion,
|
||||
or the steps to reproduce a failure (e.g. ``pytest`` and ``hypothesis`` ; example below).
|
||||
- code with retries may wish to note which iteration or timestamp raised which
|
||||
error - especially if re-raising them in an ``ExceptionGroup``
|
||||
- programming environments for novices can provide more detailed descriptions
|
||||
of various errors, and tips for resolving them (e.g. ``friendly-traceback`` ).
|
||||
|
||||
Existing approaches must pass this additional information around while keeping
|
||||
it in sync with the state of raised, and potentially caught or chained, exceptions.
|
||||
This is already error-prone, and made more difficult by :pep:`654` ``ExceptionGroup`` s,
|
||||
so the time is right for a built-in solution. We therefore propose to add a mutable
|
||||
field ``__note__`` to ``BaseException`` , which can be assigned a string - and
|
||||
if assigned, is automatically displayed in formatted tracebacks.
|
||||
|
||||
|
||||
Example usage
|
||||
-------------
|
||||
|
||||
>>> try:
|
||||
... raise TypeError('bad type')
|
||||
... except Exception as e:
|
||||
... e.__note__ = 'Add some information'
|
||||
... raise
|
||||
...
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 2, in <module>
|
||||
TypeError: bad type
|
||||
Add some information
|
||||
>>>
|
||||
|
||||
When collecting exceptions into an exception group, we may want
|
||||
to add context information for the individual errors. In the following
|
||||
example with `Hypothesis' proposed support for ExceptionGroup
|
||||
<https://github.com/HypothesisWorks/hypothesis/pull/3191>`__, each
|
||||
exception includes a note of the minimal failing example::
|
||||
|
||||
from hypothesis import given, strategies as st, target
|
||||
|
||||
@given(st.integers())
|
||||
def test(x):
|
||||
assert x < 0
|
||||
assert x > 0
|
||||
|
||||
|
||||
+ Exception Group Traceback (most recent call last):
|
||||
| File "test.py", line 4, in test
|
||||
| def test(x):
|
||||
|
|
||||
| File "hypothesis/core.py", line 1202, in wrapped_test
|
||||
| raise the_error_hypothesis_found
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
| ExceptionGroup: Hypothesis found 2 distinct failures.
|
||||
+-+---------------- 1 ----------------
|
||||
| Traceback (most recent call last):
|
||||
| File "test.py", line 6, in test
|
||||
| assert x > 0
|
||||
| ^^^^^^^^^^^^
|
||||
| AssertionError: assert -1 > 0
|
||||
|
|
||||
| Falsifying example: test(
|
||||
| x=-1,
|
||||
| )
|
||||
+---------------- 2 ----------------
|
||||
| Traceback (most recent call last):
|
||||
| File "test.py", line 5, in test
|
||||
| assert x < 0
|
||||
| ^^^^^^^^^^^^
|
||||
| AssertionError: assert 0 < 0
|
||||
|
|
||||
| Falsifying example: test(
|
||||
| x=0,
|
||||
| )
|
||||
+------------------------------------
|
||||
|
||||
|
||||
Specification
|
||||
=============
|
||||
|
||||
``BaseException`` gains a new mutable attribute ``__note__`` , which defaults to
|
||||
``None`` and may have a string assigned. When an exception with a note is displayed,
|
||||
the note is displayed immediately after the exception.
|
||||
|
||||
Assigning a new string value overrides an existing note; if concatenation is desired
|
||||
users are responsible for implementing it with e.g.::
|
||||
|
||||
e.__note__ = msg if e.__note__ is None else e.__note__ + "\n" + msg
|
||||
|
||||
It is an error to assign a non-string-or-``None`` value to ``__note__`` ,
|
||||
or to attempt to delete the attribute.
|
||||
|
||||
``BaseExceptionGroup.subgroup`` and ``BaseExceptionGroup.split``
|
||||
copy the ``__note__`` of the original exception group to the parts.
|
||||
|
||||
|
||||
Backwards Compatibility
|
||||
=======================
|
||||
|
||||
System-defined or "dunder" names (following the pattern ``__*__`` ) are part of the
|
||||
language specification, with unassigned names reserved for future use and subject
|
||||
to breakage without warning [1]_.
|
||||
|
||||
We are also unaware of any code which *would* be broken by adding ``__note__`` ;
|
||||
assigning to a ``.__note__`` attribute already *works* on current versions of
|
||||
Python - the note just won't be displayed with the traceback and exception message.
|
||||
|
||||
|
||||
|
||||
How to Teach This
|
||||
=================
|
||||
|
||||
The ``__note__`` attribute will be documented as part of the language standard,
|
||||
and explained as part of the tutorial "Errors and Exceptions" [2]_.
|
||||
|
||||
|
||||
|
||||
Reference Implementation
|
||||
========================
|
||||
|
||||
``BaseException.__note__`` was implemented in [3]_ and released in CPython 3.11.0a3,
|
||||
following discussions related to :pep:`654`. [4]_ [5]_ [6]_
|
||||
|
||||
|
||||
|
||||
Rejected Ideas
|
||||
==============
|
||||
|
||||
Use ``print()`` (or ``logging`` , etc.)
|
||||
---------------------------------------
|
||||
Reporting explanatory or contextual information about an error by printing or logging
|
||||
has historically been an acceptable workaround. However, we dislike the way this
|
||||
separates the content from the exception object it refers to - which can lead to
|
||||
"orphan" reports if the error was caught and handled later, or merely significant
|
||||
difficulties working out which explanation corresponds to which error.
|
||||
The new ``ExceptionGroup`` type intensifies these existing challenges.
|
||||
|
||||
Keeping the ``__note__`` attached to the exception object, like the traceback,
|
||||
eliminates these problems.
|
||||
|
||||
|
||||
``raise Wrapper(explanation) from err``
|
||||
---------------------------------------
|
||||
An alternative pattern is to use exception chaining: by raising a 'wrapper' exception
|
||||
containing the context or explanation ``from`` the current exception, we avoid the
|
||||
separation challenges from ``print()`` . However, this has two key problems.
|
||||
|
||||
First, it changes the type of the exception, which is often a breaking change for
|
||||
downstream code. We consider *always* raising a ``Wrapper`` exception unacceptably
|
||||
inelegant; but because custom exception types might have any number of required
|
||||
arguments we can't always create an instance of the *same* type with our explanation.
|
||||
In cases where the exact exception type is known this can work, such as the standard
|
||||
library ``http.client`` code [7]_, but not for libraries which call user code.
|
||||
|
||||
Second, exception chaining reports several lines of additional detail, which are
|
||||
distracting for experienced users and can be very confusing for beginners.
|
||||
For example, six of the eleven lines reported for this simple example relate to
|
||||
exception chaining, and are unnecessary with ``BaseException.__note__`` :
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Explanation(Exception):
|
||||
def __str__(self):
|
||||
return "\n" + str(self)
|
||||
|
||||
try:
|
||||
raise AssertionError("Failed!")
|
||||
except Exception as e:
|
||||
raise Explanation("You can reproduce this error by ...") from e
|
||||
|
||||
.. code-block::
|
||||
|
||||
$ python example.py
|
||||
Traceback (most recent call last):
|
||||
File "example.py", line 6, in <module>
|
||||
raise AssertionError(why)
|
||||
AssertionError: Failed!
|
||||
# These lines are
|
||||
The above exception was the direct cause of the following exception: # confusing for new
|
||||
# users, and they
|
||||
Traceback (most recent call last): # only exist due
|
||||
File "example.py", line 8, in <module> # to implementation
|
||||
raise Explanation(msg) from e # constraints :-(
|
||||
Explanation: # Hence this PEP!
|
||||
You can reproduce this error by ...
|
||||
|
||||
|
||||
Subclass Exception and add ``__note__`` downstream
|
||||
--------------------------------------------------
|
||||
Traceback printing is built into the C code, and reimplemented in pure Python in
|
||||
traceback.py. To get ``err.__note__`` printed from a downstream implementation
|
||||
would *also* require writing custom traceback-printing code; while this could
|
||||
be shared between projects and reuse some pieces of traceback.py we prefer to
|
||||
implement this once, upstream.
|
||||
|
||||
Custom exception types could implement their ``__str__`` method to include our
|
||||
proposed ``__note__`` semantics, but this would be rarely and inconsistently
|
||||
applicable.
|
||||
|
||||
|
||||
Store notes in ``ExceptionGroup`` s
|
||||
-----------------------------------
|
||||
Initial discussions proposed making a more focussed change by thinking about how to
|
||||
associate messages with the nested exceptions in ``ExceptionGroup`` s, such as a list
|
||||
of notes or mapping of exceptions to notes. However, this would force a remarkably
|
||||
awkward API and retains a lesser form of the cross-referencing problem discussed
|
||||
under "use ``print()`` " above; if this PEP is rejected we prefer the status quo.
|
||||
Finally, of course, ``__note__`` is not only useful with ``ExceptionGroup`` s!
|
||||
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
.. [1] https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
|
||||
.. [2] https://github.com/python/cpython/pull/30158
|
||||
.. [3] https://github.com/python/cpython/pull/29880
|
||||
.. [4] https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/9
|
||||
.. [5] https://github.com/python/cpython/pull/28569#discussion_r721768348
|
||||
.. [6] https://bugs.python.org/issue45607
|
||||
.. [7] https://github.com/python/cpython/blob/69ef1b59983065ddb0b712dac3b04107c5059735/Lib/http/client.py#L596-L597
|
||||
|
||||
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This document is placed in the public domain or under the
|
||||
CC0-1.0-Universal license, whichever is more permissive.
|
||||
|
||||
|
||||
..
|
||||
Local Variables:
|
||||
mode: indented-text
|
||||
indent-tabs-mode: nil
|
||||
sentence-end-double-space: t
|
||||
fill-column: 70
|
||||
coding: utf-8
|
||||
End:
|
Loading…
Reference in New Issue