python-peps/pep-0678.rst

360 lines
14 KiB
ReStructuredText
Raw Normal View History

PEP: 678
Title: Enriching Exceptions with Notes
Author: Zac Hatfield-Dodds <zac@zhd.dev>
Sponsor: Irit Katriel
Discussions-To: https://discuss.python.org/t/pep-678-enriching-exceptions-with-notes/13374
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Requires: 654
Created: 20-Dec-2021
Python-Version: 3.11
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.
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``.
- programming environments for novices can provide more detailed descriptions
2022-01-26 13:07:30 -05:00
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.
Example usage
-------------
>>> try:
... raise TypeError('bad type')
... except Exception as e:
... e.__note__ = 'Add some information'
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: bad type
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
<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,
| )
+------------------------------------
2022-01-26 13:07:30 -05:00
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
<https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/26>`__
instead (or additionally) choosing a convention for an attribute, e.g.
``err._parse_errors = ...`` on the error or ``ExceptionGroup``.
2022-01-26 13:07:30 -05:00
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. [1]_
2022-01-26 13:07:30 -05:00
Specification
=============
2022-01-26 13:07:30 -05:00
``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.
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.
``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
<https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers>`__.
2022-01-26 13:07:30 -05:00
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.
How to Teach This
=================
The ``__note__`` attribute will be documented as part of the language standard,
and explained as part of `the "Errors and Exceptions" tutorial
<https://github.com/python/cpython/pull/30441>`__.
Reference Implementation
========================
``BaseException.__note__`` was `implemented in <https://github.com/python/cpython/pull/29880>`__ and released in CPython
3.11.0a3, following discussions related to :pep:`654`. [2]_
Rejected Ideas
==============
2022-01-26 13:07:30 -05:00
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.
Keeping the ``__note__`` attached to the exception object, like the traceback,
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
<https://github.com/python/cpython/blob/69ef1b59983065ddb0b712dac3b04107c5059735/Lib/http/client.py#L596-L597>`__,
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
2022-01-26 13:07:30 -05:00
exception chaining, and are unnecessary with ``BaseException.__note__``:
.. code-block:: python
class Explanation(Exception):
def __str__(self):
return "\n" + str(self)
try:
raise AssertionError("Failed!")
except Exception as e:
raise Explanation("You can reproduce this error by ...") from e
.. code-block::
$ python example.py
Traceback (most recent call last):
File "example.py", line 6, in <module>
raise AssertionError(why)
AssertionError: Failed!
# 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 <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 exception
chaining rather than** ``__note__``.
2022-01-26 13:07:30 -05:00
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.
Custom exception types could implement their ``__str__`` method to include our
proposed ``__note__`` semantics, but this would be rarely and inconsistently
applicable.
2022-01-26 13:07:30 -05:00
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!
2022-01-26 13:07:30 -05:00
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__``.
2022-01-26 13:07:30 -05:00
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.
2022-01-26 13:07:30 -05:00
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.
2022-01-26 13:07:30 -05:00
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.
2022-01-26 13:07:30 -05:00
Add a helper function ``contextlib.add_exc_note()``
---------------------------------------------------
It `was suggested
<https://www.reddit.com/r/Python/comments/rmrvxv/pep_678_enriching_exceptions_with_notes/hptbul1/>`__
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.
2022-01-26 13:07:30 -05:00
.. code-block:: python
@contextlib.contextmanager def add_exc_note(note: str):
2022-01-26 13:07:30 -05:00
try:
yield
except Exception as err:
if err.__note__ is None:
err.__note__ = note
else:
err.__note__ = err.__note__ + "\n\n" + note
raise
with add_exc_note(f"While attempting to frobnicate {item=}"):
frobnicate_or_raise(item)
Augment the ``raise`` statement
-------------------------------
One discussion proposed ``raise Exception() with "note contents"``, but this
does not address the original motivation of compatibility with
``ExceptionGroup``.
2022-01-26 13:07:30 -05:00
Furthermore, we do not believe that the problem we are solving requires or
justifies new language syntax.
2022-01-26 13:07:30 -05:00
References
==========
.. [1] this principle was established in the 2003 mail thread which led to :pep:`3134`,
2022-01-26 13:07:30 -05:00
and included a proposal for a group-of-exceptions type!
https://mail.python.org/pipermail/python-dev/2003-January/032492.html
.. [2] particularly those at https://bugs.python.org/issue45607,
https://discuss.python.org/t/accepting-pep-654-exception-groups-and-except/10813/9,
https://github.com/python/cpython/pull/28569#discussion_r721768348, and
Copyright
=========
This document is placed in the public domain or under the
CC0-1.0-Universal license, whichever is more permissive.
..
Local Variables:
mode: indented-text
indent-tabs-mode: nil
sentence-end-double-space: t
fill-column: 70
coding: utf-8
End: