diff --git a/pep-0678.rst b/pep-0678.rst index 31e4648634d..3aa8b66779e 100644 --- a/pep-0678.rst +++ b/pep-0678.rst @@ -15,38 +15,41 @@ Post-History: 2022-01-27 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, or included in an ``ExceptionGroup``, 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. +error which has occurred. Because further information may be available when +the exception is caught and re-raised, or included in an ``ExceptionGroup``, +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 and ``cattrs`` packages, and common code -patterns with retries. +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 and ``cattrs`` packages, 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 which retries an operation on error may wish to associate an iteration, timestamp, - or other explanation with each of several errors - especially if re-raising them in - an ``ExceptionGroup``. +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 which retries an operation on error may wish to associate an iteration, + timestamp, or other explanation with each of several errors - 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. +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 @@ -64,11 +67,11 @@ Example usage 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 -`__, each -exception includes a note of the minimal failing example:: +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 +`__, each exception +includes a note of the minimal failing example:: from hypothesis import given, strategies as st, target @@ -111,44 +114,48 @@ exception includes a note of the minimal failing example:: Non-goals --------- -``__note__`` is *not* intended to carry structured data. If your note is for use by -a program rather than display to a human, we recommend instead (or additionally) choosing a convention -for an attribute like e.g. ``err._parse_errors = ...`` on the error or ``ExceptionGroup`` [1]_ [2]_ +``__note__`` is *not* intended to carry structured data. If your note is for +use by a program rather than display to a human, we recommend instead (or +additionally) choosing a convention for an attribute like e.g. +``err._parse_errors = ...`` on the error or ``ExceptionGroup`` [1]_ [2]_ -As a rule of thumb, prefer `exception chaining `__ -when the error is going to be re-raised or handled as an individual error, and prefer -``__note__`` when you are collecting multiple exception objects to handle together or later. [3]_ +As a rule of thumb, prefer `exception chaining +`__ when the +error is going to be re-raised or handled as an individual error, and prefer +``__note__`` when you are collecting multiple exception objects to handle +together or later. [3]_ 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. +``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.:: +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. +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. +``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 [4]_. +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 [4]_. 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. +Python - the note just won't be displayed with the traceback and exception +message. @@ -163,8 +170,8 @@ and explained as part of the tutorial "Errors and Exceptions" [5]_. Reference Implementation ======================== -``BaseException.__note__`` was implemented in [6]_ and released in CPython 3.11.0a3, -following discussions related to :pep:`654`. [7]_ [8]_ [9]_ +``BaseException.__note__`` was implemented in [6]_ and released in CPython +3.11.0a3, following discussions related to :pep:`654`. [7]_ [8]_ [9]_ @@ -173,12 +180,13 @@ 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. +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. @@ -186,19 +194,21 @@ 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 [10]_, 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. +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 [10]_, 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__``: @@ -220,26 +230,26 @@ exception chaining, and are unnecessary with ``BaseException.__note__``: File "example.py", line 6, in 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 # to implementation - raise Explanation(msg) from e # constraints :-( - Explanation: # Hence this PEP! + # These lines are + The above exception was the direct cause of ... # confusing for new + # users, and they + Traceback (most recent call last): # only exist due + File "example.py", line 8, in # 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 exception chaining rather than** ``__note__``. +**In cases where these two problems do not apply, we encourage use of exception +chaining rather than** ``__note__``. 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. +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 @@ -248,50 +258,52 @@ 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! +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! Possible Future Enhancements ============================ -In addition to rejected alternatives, there have been a range of suggestions which -we believe should be deferred to a future version, when we have more experience with -the uses (and perhaps misuses) of ``__note__``. +In addition to rejected alternatives, there have been a range of suggestions +which we believe should be deferred to a future version, when we have more +experience with the uses (and perhaps misuses) of ``__note__``. Allow any object, and cast to string for display ------------------------------------------------ -We have not identified any scenario where libraries would want to do anything but either -concatenate or replace notes, and so the additional complexity and interoperability -challenges do not seem justified. +We have not identified any scenario where libraries would want to do anything +but either concatenate or replace notes, and so the additional complexity and +interoperability challenges do not seem justified. -Permitting any object would also force any future structured API to change the behaviour -of already-legal code, whereas expanding the permitted contents of ``__note__`` from strings -to include other objects is fully backwards-compatible. In the absence of any proposed -use-case (see also `Non-goals`_), we prefer to begin with a restrictive API that can -be relaxed later. +Permitting any object would also force any future structured API to change the +behaviour of already-legal code, whereas expanding the permitted contents of +``__note__`` from strings to include other objects is fully +backwards-compatible. In the absence of any proposed use-case (see also +`Non-goals`_), we prefer to begin with a restrictive API that can be relaxed +later. -We also note that converting an object to a string may raise an exception. It's more helpful -for the traceback to point to the location where the note is attached to the exception, -rather than where the exception and note are being formatted for display after propagation. +We also note that converting an object to a string may raise an exception. +It's more helpful for the traceback to point to the location where the note is +attached to the exception, rather than where the exception and note are being +formatted for display after propagation. Add a helper function ``contextlib.add_exc_note()`` --------------------------------------------------- -It was suggested [11]_ that we add a utility such as the one below to the standard -library. We are open to this idea, but do not see it as a core part of the -proposal of this PEP as it can be added as an enhancement later. +It was suggested [11]_ that we add a utility such as the one below to the +standard library. We are open to this idea, but do not see it as a core part of +the proposal of this PEP as it can be added as an enhancement later. .. code-block:: python - @contextlib.contextmanager - def add_exc_note(note: str): + @contextlib.contextmanager def add_exc_note(note: str): try: yield except Exception as err: @@ -308,10 +320,11 @@ proposal of this PEP as it can be added as an enhancement later. Augment the ``raise`` statement ------------------------------- One discussion proposed ``raise Exception() with "note contents"``, but this -does not address the original motivation of compatibility with ``ExceptionGroup``. +does not address the original motivation of compatibility with +``ExceptionGroup``. -Furthermore, we do not believe that the problem we are solving requires or justifies -new language syntax. +Furthermore, we do not believe that the problem we are solving requires or +justifies new language syntax. References ==========