Important
This PEP is a historical document. The up-to-date, canonical documentation can now be found atBaseException.add_note() andBaseException.__notes__.
×
SeeEnriching Exceptions with Notes for a user-focused tutorial.
SeePEP 1 for how to propose changes.
Exception objects are typically initialized with a message that describes theerror which has occurred. Because further information may be available whenthe exception is caught and re-raised, or included in anExceptionGroup,this PEP proposes to addBaseException.add_note(note), a.__notes__ attribute holding a list of notes so added, and toupdate the builtin traceback formatting code to include notes in the formattedtraceback following the exception string.
This is particularly useful in relation toPEP 654ExceptionGroups,which make previous workarounds ineffective or confusing. Use cases have beenidentified in the standard library, Hypothesis andcattrs packages, andcommon code patterns with retries.
When an exception is created in order to be raised, it is usually initializedwith information that describes the error that has occurred. There are caseswhere it is useful to add information after the exception was caught. Forexample,
pytest andhypothesis; example below).ExceptionGroup.Existing approaches must pass this additional information around while keepingit in sync with the state of raised, and potentially caught or chained,exceptions. This is already error-prone, and made more difficult byPEP 654ExceptionGroups, so the time is right for a built-in solution. Wetherefore propose to add:
BaseException.add_note(note:str),BaseException.__notes__, a list of note strings added using.add_note(), and>>>try:...raiseTypeError('bad type')...exceptExceptionase:...e.add_note('Add some information')...raise...Traceback (most recent call last): File"<stdin>", line2, in<module>TypeError:bad typeAdd some information>>>
When collecting exceptions into an exception group, we may want to add contextinformation for the individual errors. In the following example withHypothesis’ proposed support for ExceptionGroup, each exceptionincludes a note of the minimal failing example:
fromhypothesisimportgiven,strategiesasst,target@given(st.integers())deftest(x):assertx<0assertx>0+ExceptionGroupTraceback(mostrecentcalllast):|File"test.py",line4,intest|deftest(x):||File"hypothesis/core.py",line1202,inwrapped_test|raisethe_error_hypothesis_found|^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^|ExceptionGroup:Hypothesisfound2distinctfailures.+-+----------------1----------------|Traceback(mostrecentcalllast):|File"test.py",line6,intest|assertx>0|^^^^^^^^^^^^|AssertionError:assert-1>0||Falsifyingexample:test(|x=-1,|)+----------------2----------------|Traceback(mostrecentcalllast):|File"test.py",line5,intest|assertx<0|^^^^^^^^^^^^|AssertionError:assert0<0||Falsifyingexample:test(|x=0,|)+------------------------------------
Tracking multiple notes as a list, rather than by concatenating strings whennotes are added, is intended to maintain the distinction between theindividual notes. This might be required in specialized use cases, suchas translation of the notes by packages likefriendly-traceback.
However,__notes__ isnot intended to carry structured data. If yournote is for use by a program rather than display to a human,we recommendinstead (or additionally) choosing a convention for an attribute, e.g.err._parse_errors=... on the error orExceptionGroup.
As a rule of thumb, we suggest that you should preferexception chaining when theerror is going to be re-raised or handled as an individual error, and prefer.add_note() when you want to avoid changing the exception type orare collecting multiple exception objects to handle together.[1]
BaseException gains a new method.add_note(note:str). Ifnote isa string,.add_note(note) appends it to the__notes__ list, creatingthe attribute if it does not already exist. Ifnote is not a string,.add_note() raisesTypeError.
Libraries may clear existing notes by modifying or deleting the__notes__list, if it has been created, including clearing all notes withdelerr.__notes__. This allows full control over the attached notes,without overly complicating the API or adding multiple names toBaseException.__dict__.
When an exception is displayed by the interpreter’s builtin traceback-rendering code,its notes (if there are any) appear immediately after the exception message, in the orderin which they were added, with each note starting on a new line.
If__notes__ has been created,BaseExceptionGroup.subgroup andBaseExceptionGroup.split create a new list for each new instance, containingthe same contents as the original exception group’s__notes__.
Wedo not specify the expected behaviour when users have assigned a non-listvalue to__notes__, or a list which contains non-string elements.Implementations might choose to emit warnings, discard or ignore bad values,convert them to strings, raise an exception, or do something else entirely.
System-defined or “dunder” names (following the pattern__*__) are part ofthe language specification, withunassigned names reserved for future use andsubject to breakage without warning.We are also unaware of any code whichwould be broken by adding__notes__.
We were also unable to find any code which would be broken by the addition ofBaseException.add_note(): while searching Google andGitHub finds severaldefinitionsof an.add_note() method, none of them are on a subclass ofBaseException.
Theadd_note() method and__notes__ attribute will be documented aspart of the language standard, and explained as part ofthe “Errors andExceptions” tutorial.
Following discussions related toPEP 654[2], an early version of thisproposal wasimplemented inand released in CPython 3.11.0a3, with a mutable string-or-none__note__attribute.
CPython PR #31317implements.add_note() and__notes__.
print() (orlogging, etc.)Reporting explanatory or contextual information about an error by printing orlogging has historically been an acceptable workaround. However, we dislikethe 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 towhich error. The newExceptionGroup type intensifies these existingchallenges.
Keeping the__notes__ attached to the exception object, in the same way asthe__traceback__ attribute, eliminates these problems.
raiseWrapper(explanation)fromerrAn alternative pattern is to use exception chaining: by raising a ‘wrapper’exception containing the context or explanationfrom the current exception,we avoid the separation challenges fromprint(). However, this has two keyproblems.
First, it changes the type of the exception, which is often a breaking changefor downstream code. We consideralways raising aWrapper exceptionunacceptably inelegant; but because custom exception types might have anynumber of required arguments we can’t always create an instance of thesametype with our explanation. In cases where the exact exception type is knownthis can work, such as the standard libraryhttp.clientcode,but not for libraries which call user code.
Second, exception chaining reports several lines of additional detail, whichare distracting for experienced users and can be very confusing for beginners.For example, six of the eleven lines reported for this simple example relate toexception chaining, and are unnecessary withBaseException.add_note():
classExplanation(Exception):def__str__(self):return"\n"+str(self.args[0])try:raiseAssertionError("Failed!")exceptExceptionase:raiseExplanation("You can reproduce this error by ...")frome
$ python example.pyTraceback (most recent call last):File "example.py", line 6, in <module> raise AssertionError(why)AssertionError: Failed! # These lines areThe above exception was the direct cause of ... # confusing for new # users, and theyTraceback (most recent call last): # only exist dueFile "example.py", line 8, in <module> # to implementation raise Explanation(msg) from e # constraints :-(Explanation: # Hence this PEP!You can reproduce this error by ...
In cases where these two problems do not apply, we encourage use of exceptionchaining rather than__notes__.
__note__ attributeThe first draft and implementation of this PEP defined a single attribute__note__, which defaulted toNone but could have a string assigned.This is substantially simpler if, and only if, there is at most one note.
To promote interoperability and support translation of error messages bylibraries such asfriendly-traceback, without resorting to dubious parsingheuristics, we therefore settled on the.add_note()-and-__notes__ API.
Traceback printing is built into the C code, and reimplemented in pure Pythonintraceback.py. To geterr.__notes__ printed from a downstreamimplementation wouldalso require writing custom traceback-printing code;while this could be shared between projects and reuse some pieces oftraceback.py[3] we prefer to implement this once, upstream.
Custom exception types could implement their__str__ method to include ourproposed__notes__ semantics, but this would be rarely and inconsistentlyapplicable.
Exceptions, just store them inExceptionGroupsThe initial motivation for this PEP was to associate a note with each errorin anExceptionGroup. At the cost of a remarkably awkward API and thecross-referencing problem discussedabove, thisuse-case could be supported by storing notes on theExceptionGroupinstance instead of on each exception it contains.
We believe that the cleaner interface, and other use-cases described above,are sufficient to justify the more general feature proposed by this PEP.
contextlib.add_exc_note()Itwas suggestedthat we add a utility such as the one below to the standard library. We do notsee this idea as core to the proposal of this PEP, and thus leave it for lateror downstream implementation - perhaps based on this example code:
@contextlib.contextmanagerdefadd_exc_note(note:str):try:yieldexceptExceptionaserr:err.add_note(note)raisewithadd_exc_note(f"While attempting to frobnicate{item=}"):frobnicate_or_raise(item)
raise statementOne discussion proposedraiseException()with"notecontents", but thisdoes not address the original motivation of compatibility withExceptionGroup.
Furthermore, we do not believe that the problem we are solving requires orjustifies new language syntax.
We wish to thank the many people who have assisted us through conversation,code review, design advice, and implementation: Adam Turner, Alex Grönholm,André Roberge, Barry Warsaw, Brett Cannon, CAM Gerlach, Carol Willing, Damian,Erlend Aasland, Etienne Pot, Gregory Smith, Guido van Rossum, Irit Katriel,Jelle Zijlstra, Ken Jin, Kumar Aditya, Mark Shannon, Matti Picus, PetrViktorin, Will McGugan, and pseudonymous commenters on Discord and Reddit.
exceptiongroup backport package maintains an exceptionhook and monkeypatch forTracebackException for Pythons older than 3.11,and encourage library authors to avoid creating additional and incompatiblebackports. We also reiterate our preference for builtin support whichmakes such measures unnecessary.This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0678.rst
Last modified:2025-02-01 08:55:40 GMT