Skip to content

Commit

Permalink
Structure, footnotes, formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Jan 26, 2022
1 parent d122621 commit 1b7442c
Showing 1 changed file with 73 additions and 57 deletions.
130 changes: 73 additions & 57 deletions pep-0678.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ 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
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
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.
in the standard library, Hypothesis and ``cattrs`` packages, and common code
patterns with retries.


Motivation
Expand All @@ -32,17 +33,18 @@ 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
of several possible errors - especially if re-raising them in an ``ExceptionGroup``
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`` ).
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,
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
field ``__note__`` to ``BaseException``, which can be assigned a string - and
if assigned, is automatically displayed in formatted tracebacks.


Expand Down Expand Up @@ -110,19 +112,17 @@ 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 chosing a convention
for an attribute like e.g. ``err._parse_errors = ...`` on the error or ``ExceptionGroup``
(`discussion <https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/26>`__,
`discussion <https://bugs.python.org/issue46431>`__).
for an attribute like e.g. ``err._parse_errors = ...`` on the error or ``ExceptionGroup`` [1]_ [2]_

As a rule of thumb, prefer `exception chaining <https://docs.python.org/3/tutorial/errors.html#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.
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
``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.

Expand All @@ -131,7 +131,7 @@ 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__`` ,
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``
Expand All @@ -141,11 +141,11 @@ 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
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]_.
to breakage without warning [4]_.

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 All @@ -155,23 +155,23 @@ 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]_.
and explained as part of the tutorial "Errors and Exceptions" [5]_.



Reference Implementation
========================

``BaseException.__note__`` was implemented in [3]_ and released in CPython 3.11.0a3,
following discussions related to :pep:`654`. [4]_ [5]_ [6]_
``BaseException.__note__`` was implemented in [6]_ and released in CPython 3.11.0a3,
following discussions related to :pep:`654`. [7]_ [8]_ [9]_



Rejected Ideas
==============

Use ``print()`` (or ``logging`` , etc.)
---------------------------------------
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
Expand All @@ -187,19 +187,19 @@ eliminates these problems.
---------------------------------------
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.
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.
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__`` :
exception chaining, and are unnecessary with ``BaseException.__note__``:

.. code-block:: python
Expand Down Expand Up @@ -228,8 +228,8 @@ exception chaining, and are unnecessary with ``BaseException.__note__`` :
Explanation: # Hence this PEP!
You can reproduce this error by ...
**Where these factors are not problematic, 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
Expand All @@ -245,15 +245,23 @@ proposed ``__note__`` semantics, but this would be rarely and inconsistently
applicable.


Augment the ``raise`` statement
-------------------------------
One discussion proposed ``raise Exception() with "note contents"`` , but this
does not address the original motivation of compatibility with ``ExceptionGroup`` .
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!

Furthermore, this would be an additional feature over adding the attribute and
modifying the traceback-formatting code, and we do not expect ``__note__`` to be
so frequently-used as to justify new syntax or even a new ``.with_note()`` method
(analogous to ``.with_traceback()`` ).


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__``.


Allow any object, and cast to string for display
Expand All @@ -262,16 +270,18 @@ We have not identified any scenario where libraries would want to do anything bu
concatenate or replace notes, and so the additional complexity and interoperability
challenges do not seem justified.

Variations on this have also been proposed for forward-compatibility with a future
structured API, but in the absence of any proposed use-case (see also "Non-goals")
we strongly prefer to keep things simple - and note that implementing any nontrival
future behavior would then break backwards-compatibility.
Permitting any object would also force any future structured API to change the behaviour
of already-legal code, whereas expanding the legal 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 strongly prefer to keep things simple.


Add a helper function ``contexlib.add_exc_note()``
--------------------------------------------------
We don't expect notes to be used frequently enough to justify this either, but
provide the following extensible recipe::
provide the following extensible recipe:

.. code-block:: python
@contextlib.contextmanager
def add_exc_note(note: str):
Expand All @@ -288,27 +298,33 @@ provide the following extensible recipe::
frobnicate_or_raise(item)
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!
Augment the ``raise`` statement
-------------------------------
One discussion proposed ``raise Exception() with "note contents"``, but this
does not address the original motivation of compatibility with ``ExceptionGroup``.

Furthermore, this would be an additional feature over adding the attribute and
modifying the traceback-formatting code, and we do not expect ``__note__`` to be
so frequently-used as to justify new syntax, or even a new ``.with_note()`` method
analogous to ``.with_traceback()``.



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
.. [1] https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/26
.. [2] https://bugs.python.org/issue46431
.. [3] this principle was established in the 2003 mail thread which led to :pep:`3134`,
and included a proposal for a group-of-exceptions type!
https://mail.python.org/pipermail/python-dev/2003-January/032492.html
.. [4] https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
.. [5] https://github.com/python/cpython/pull/30158
.. [6] https://github.com/python/cpython/pull/29880
.. [7] https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/9
.. [8] https://github.com/python/cpython/pull/28569#discussion_r721768348
.. [9] https://bugs.python.org/issue45607
.. [10] https://github.com/python/cpython/blob/69ef1b59983065ddb0b712dac3b04107c5059735/Lib/http/client.py#L596-L597
Expand Down

0 comments on commit 1b7442c

Please sign in to comment.