Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 67 additions & 52 deletions pep-9999.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ Post-History:

Abstract
========
When an exception is created and raised, it is usually initialized with information
that describes the error that has occurred. To support cases where it is useful to
add information *after* the exception was caught, this PEP proposes adding a
``.__note__`` attribute to exceptions for display following the message.
Exception objects are typically initialized with a message that describes the
error which has occured. 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
Expand All @@ -30,14 +31,16 @@ 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`).
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`)
of various errors, and tips for resolving them (e.g. `friendly-traceback`).

Existing approaches don't work well with :pep:`654` ``ExceptionGroup``s, so the
time is right for a built-in solution. We therefore propose to add a mutable
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.

Expand All @@ -58,45 +61,48 @@ Example usage
>>>

When collecting exceptions into an exception group, we may want
to add context information for the individual errors. In the following each
exception in the group has a note indicating when this error has occurred::

>>> def f():
... raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.__note__ = f'Happened in Iteration {i+1}'
... excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| ExceptionGroup: We have some problems
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| File "<stdin>", line 2, in f
| OSError: operation failed
| Happened in Iteration 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| File "<stdin>", line 2, in f
| OSError: operation failed
| Happened in Iteration 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| File "<stdin>", line 2, in f
| OSError: operation failed
| Happened in Iteration 3
+------------------------------------
>>>

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
Expand All @@ -122,7 +128,9 @@ System-defined or "dunder" names (following the pattern ``__*__``) are part of t
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__``.
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.



Expand Down Expand Up @@ -164,7 +172,7 @@ An alternative pattern is to use exception chaining: by raising a 'wrapper' exce
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 an breaking change for
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 which might have any number of required
arguments we can't always create an instance of the *same* type with our explanation.
Expand Down Expand Up @@ -213,12 +221,19 @@ 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
----------------------------------
``__note__`` is not *only* useful with ``ExceptionGroup``s, and as with printing
we prefer to keep this information directly with the exception it relates to
rather than storing a separate collection of notes and cross-referencing them.
Initial discussions focussed on making a small 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!



Expand Down