Skip to content

Commit 8c43285

Browse files
Zac-HDiritkatriel
andauthored
PEP 678: Enriching Exceptions with Notes (#2201)
Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com>
1 parent 886624a commit 8c43285

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ pep-0674.rst @vstinner
533533
pep-0675.rst @jellezijlstra
534534
pep-0676.rst @Mariatta
535535
pep-0677.rst @gvanrossum
536+
pep-0678.rst @iritkatriel
536537
# ...
537538
# pep-0754.txt
538539
# ...

pep-0678.rst

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
PEP: 678
2+
Title: Enriching Exceptions with Notes
3+
Author: Zac Hatfield-Dodds <zac@zhd.dev>
4+
Sponsor: Irit Katriel
5+
Status: Draft
6+
Type: Standards Track
7+
Content-Type: text/x-rst
8+
Requires: 654
9+
Created: 20-Dec-2021
10+
Python-Version: 3.11
11+
Post-History:
12+
13+
14+
Abstract
15+
========
16+
Exception objects are typically initialized with a message that describes the
17+
error which has occurred. Because further information may be available when the
18+
exception is caught and re-raised, this PEP proposes to add a ``.__note__``
19+
attribute and update the builtin traceback formatting code to include it in
20+
the formatted traceback following the exception string.
21+
22+
This is particularly useful in relation to :pep:`654` ``ExceptionGroup`` s, which
23+
make previous workarounds ineffective or confusing. Use cases have been identified
24+
in the standard library, Hypothesis package, and common code patterns with retries.
25+
26+
27+
Motivation
28+
==========
29+
When an exception is created in order to be raised, it is usually initialized
30+
with information that describes the error that has occurred. There are cases
31+
where it is useful to add information after the exception was caught.
32+
For example,
33+
34+
- testing libraries may wish to show the values involved in a failing assertion,
35+
or the steps to reproduce a failure (e.g. ``pytest`` and ``hypothesis`` ; example below).
36+
- code with retries may wish to note which iteration or timestamp raised which
37+
error - especially if re-raising them in an ``ExceptionGroup``
38+
- programming environments for novices can provide more detailed descriptions
39+
of various errors, and tips for resolving them (e.g. ``friendly-traceback`` ).
40+
41+
Existing approaches must pass this additional information around while keeping
42+
it in sync with the state of raised, and potentially caught or chained, exceptions.
43+
This is already error-prone, and made more difficult by :pep:`654` ``ExceptionGroup`` s,
44+
so the time is right for a built-in solution. We therefore propose to add a mutable
45+
field ``__note__`` to ``BaseException`` , which can be assigned a string - and
46+
if assigned, is automatically displayed in formatted tracebacks.
47+
48+
49+
Example usage
50+
-------------
51+
52+
>>> try:
53+
... raise TypeError('bad type')
54+
... except Exception as e:
55+
... e.__note__ = 'Add some information'
56+
... raise
57+
...
58+
Traceback (most recent call last):
59+
File "<stdin>", line 2, in <module>
60+
TypeError: bad type
61+
Add some information
62+
>>>
63+
64+
When collecting exceptions into an exception group, we may want
65+
to add context information for the individual errors. In the following
66+
example with `Hypothesis' proposed support for ExceptionGroup
67+
<https://github.com/HypothesisWorks/hypothesis/pull/3191>`__, each
68+
exception includes a note of the minimal failing example::
69+
70+
from hypothesis import given, strategies as st, target
71+
72+
@given(st.integers())
73+
def test(x):
74+
assert x < 0
75+
assert x > 0
76+
77+
78+
+ Exception Group Traceback (most recent call last):
79+
| File "test.py", line 4, in test
80+
| def test(x):
81+
|
82+
| File "hypothesis/core.py", line 1202, in wrapped_test
83+
| raise the_error_hypothesis_found
84+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
85+
| ExceptionGroup: Hypothesis found 2 distinct failures.
86+
+-+---------------- 1 ----------------
87+
| Traceback (most recent call last):
88+
| File "test.py", line 6, in test
89+
| assert x > 0
90+
| ^^^^^^^^^^^^
91+
| AssertionError: assert -1 > 0
92+
|
93+
| Falsifying example: test(
94+
| x=-1,
95+
| )
96+
+---------------- 2 ----------------
97+
| Traceback (most recent call last):
98+
| File "test.py", line 5, in test
99+
| assert x < 0
100+
| ^^^^^^^^^^^^
101+
| AssertionError: assert 0 < 0
102+
|
103+
| Falsifying example: test(
104+
| x=0,
105+
| )
106+
+------------------------------------
107+
108+
109+
Specification
110+
=============
111+
112+
``BaseException`` gains a new mutable attribute ``__note__`` , which defaults to
113+
``None`` and may have a string assigned. When an exception with a note is displayed,
114+
the note is displayed immediately after the exception.
115+
116+
Assigning a new string value overrides an existing note; if concatenation is desired
117+
users are responsible for implementing it with e.g.::
118+
119+
e.__note__ = msg if e.__note__ is None else e.__note__ + "\n" + msg
120+
121+
It is an error to assign a non-string-or-``None`` value to ``__note__`` ,
122+
or to attempt to delete the attribute.
123+
124+
``BaseExceptionGroup.subgroup`` and ``BaseExceptionGroup.split``
125+
copy the ``__note__`` of the original exception group to the parts.
126+
127+
128+
Backwards Compatibility
129+
=======================
130+
131+
System-defined or "dunder" names (following the pattern ``__*__`` ) are part of the
132+
language specification, with unassigned names reserved for future use and subject
133+
to breakage without warning [1]_.
134+
135+
We are also unaware of any code which *would* be broken by adding ``__note__`` ;
136+
assigning to a ``.__note__`` attribute already *works* on current versions of
137+
Python - the note just won't be displayed with the traceback and exception message.
138+
139+
140+
141+
How to Teach This
142+
=================
143+
144+
The ``__note__`` attribute will be documented as part of the language standard,
145+
and explained as part of the tutorial "Errors and Exceptions" [2]_.
146+
147+
148+
149+
Reference Implementation
150+
========================
151+
152+
``BaseException.__note__`` was implemented in [3]_ and released in CPython 3.11.0a3,
153+
following discussions related to :pep:`654`. [4]_ [5]_ [6]_
154+
155+
156+
157+
Rejected Ideas
158+
==============
159+
160+
Use ``print()`` (or ``logging`` , etc.)
161+
---------------------------------------
162+
Reporting explanatory or contextual information about an error by printing or logging
163+
has historically been an acceptable workaround. However, we dislike the way this
164+
separates the content from the exception object it refers to - which can lead to
165+
"orphan" reports if the error was caught and handled later, or merely significant
166+
difficulties working out which explanation corresponds to which error.
167+
The new ``ExceptionGroup`` type intensifies these existing challenges.
168+
169+
Keeping the ``__note__`` attached to the exception object, like the traceback,
170+
eliminates these problems.
171+
172+
173+
``raise Wrapper(explanation) from err``
174+
---------------------------------------
175+
An alternative pattern is to use exception chaining: by raising a 'wrapper' exception
176+
containing the context or explanation ``from`` the current exception, we avoid the
177+
separation challenges from ``print()`` . However, this has two key problems.
178+
179+
First, it changes the type of the exception, which is often a breaking change for
180+
downstream code. We consider *always* raising a ``Wrapper`` exception unacceptably
181+
inelegant; but because custom exception types might have any number of required
182+
arguments we can't always create an instance of the *same* type with our explanation.
183+
In cases where the exact exception type is known this can work, such as the standard
184+
library ``http.client`` code [7]_, but not for libraries which call user code.
185+
186+
Second, exception chaining reports several lines of additional detail, which are
187+
distracting for experienced users and can be very confusing for beginners.
188+
For example, six of the eleven lines reported for this simple example relate to
189+
exception chaining, and are unnecessary with ``BaseException.__note__`` :
190+
191+
.. code-block:: python
192+
193+
class Explanation(Exception):
194+
def __str__(self):
195+
return "\n" + str(self)
196+
197+
try:
198+
raise AssertionError("Failed!")
199+
except Exception as e:
200+
raise Explanation("You can reproduce this error by ...") from e
201+
202+
.. code-block::
203+
204+
$ python example.py
205+
Traceback (most recent call last):
206+
File "example.py", line 6, in <module>
207+
raise AssertionError(why)
208+
AssertionError: Failed!
209+
# These lines are
210+
The above exception was the direct cause of the following exception: # confusing for new
211+
# users, and they
212+
Traceback (most recent call last): # only exist due
213+
File "example.py", line 8, in <module> # to implementation
214+
raise Explanation(msg) from e # constraints :-(
215+
Explanation: # Hence this PEP!
216+
You can reproduce this error by ...
217+
218+
219+
Subclass Exception and add ``__note__`` downstream
220+
--------------------------------------------------
221+
Traceback printing is built into the C code, and reimplemented in pure Python in
222+
traceback.py. To get ``err.__note__`` printed from a downstream implementation
223+
would *also* require writing custom traceback-printing code; while this could
224+
be shared between projects and reuse some pieces of traceback.py we prefer to
225+
implement this once, upstream.
226+
227+
Custom exception types could implement their ``__str__`` method to include our
228+
proposed ``__note__`` semantics, but this would be rarely and inconsistently
229+
applicable.
230+
231+
232+
Store notes in ``ExceptionGroup`` s
233+
-----------------------------------
234+
Initial discussions proposed making a more focussed change by thinking about how to
235+
associate messages with the nested exceptions in ``ExceptionGroup`` s, such as a list
236+
of notes or mapping of exceptions to notes. However, this would force a remarkably
237+
awkward API and retains a lesser form of the cross-referencing problem discussed
238+
under "use ``print()`` " above; if this PEP is rejected we prefer the status quo.
239+
Finally, of course, ``__note__`` is not only useful with ``ExceptionGroup`` s!
240+
241+
242+
243+
References
244+
==========
245+
246+
.. [1] https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
247+
.. [2] https://github.com/python/cpython/pull/30158
248+
.. [3] https://github.com/python/cpython/pull/29880
249+
.. [4] https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/9
250+
.. [5] https://github.com/python/cpython/pull/28569#discussion_r721768348
251+
.. [6] https://bugs.python.org/issue45607
252+
.. [7] https://github.com/python/cpython/blob/69ef1b59983065ddb0b712dac3b04107c5059735/Lib/http/client.py#L596-L597
253+
254+
255+
256+
Copyright
257+
=========
258+
259+
This document is placed in the public domain or under the
260+
CC0-1.0-Universal license, whichever is more permissive.
261+
262+
263+
..
264+
Local Variables:
265+
mode: indented-text
266+
indent-tabs-mode: nil
267+
sentence-end-double-space: t
268+
fill-column: 70
269+
coding: utf-8
270+
End:

0 commit comments

Comments
 (0)