Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 40 additions & 18 deletions peps/pep-0785.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ As :pep:`654` :class:`ExceptionGroup` has come into widespread use across the
Python community, some common but awkward patterns have emerged. We therefore
propose adding two new methods to exception objects:

- :meth:`!BaseExceptionGroup.flat_exceptions`, returning the 'leaf' exceptions as
- :meth:`!BaseExceptionGroup.leaf_exceptions`, returning the 'leaf' exceptions as
a list, with each traceback composited from any intermediate groups.

- :meth:`!BaseException.preserve_context`, a context manager which
Expand All @@ -39,10 +39,10 @@ often write code to process or respond to individual leaf exceptions, for
example when implementing middleware, error logging, or response handlers in
a web framework.

`Searching GitHub`__ found four implementations of :meth:`!flat_exceptions` by
`Searching GitHub`__ found four implementations of :meth:`!leaf_exceptions` by
various names in the first sixty hits, of which none handle
tracebacks.\ [#numbers]_ The same search found thirteen cases where
:meth:`!.flat_exceptions` could be used. We therefore believe that providing
:meth:`!.leaf_exceptions` could be used. We therefore believe that providing
a method on the :class:`BaseException` type with proper traceback preservation
will improve error-handling and debugging experiences across the ecosystem.

Expand All @@ -55,7 +55,7 @@ unwrap ``HTTPException`` if that is the sole leaf of a group:
.. code-block:: python

except* HTTPException as group:
first, *rest = group.flat_exceptions() # get the whole traceback :-)
first, *rest = group.leaf_exceptions() # get the whole traceback :-)
if not rest:
raise first
raise
Expand All @@ -78,12 +78,12 @@ readable, and easy-to-use solution for these cases.
Specification
=============

A new method ``flat_exceptions()`` will be added to ``BaseExceptionGroup``, with the
A new method ``leaf_exceptions()`` will be added to ``BaseExceptionGroup``, with the
following signature:

.. code-block:: python

def flat_exceptions(self, *, fix_tracebacks=True) -> list[BaseException]:
def leaf_exceptions(self, *, fix_tracebacks=True) -> list[BaseException]:
"""
Return a flat list of all 'leaf' exceptions in the group.

Expand Down Expand Up @@ -118,7 +118,7 @@ Usage example:
try:
user_code_here()
except* HTTPException as group:
first, *rest = group.flat_exceptions()
first, *rest = group.leaf_exceptions()
if rest:
raise # handled by internal-server-error middleware
... # logging, cache updates, etc.
Expand All @@ -142,12 +142,12 @@ Backwards Compatibility

Adding new methods to built-in classes, especially those as widely used as
``BaseException``, can have substantial impacts. However, GitHub search shows
no collisions for these method names (`zero hits`__ and
no collisions for these method names (`zero hits`__\ [#naming]_ and
`three unrelated hits`__ respectively). If user-defined methods with these
names exist in private code they will shadow those proposed in the PEP,
without changing runtime behavior.

__ https://github.com/search?q=%2F%5C.flat_exceptions%5C%28%2F+language%3APython&type=code
__ https://github.com/search?q=%2F%5C.leaf_exceptions%5C%28%2F+language%3APython&type=code
__ https://github.com/search?q=%2F%5C.preserve_context%5C%28%2F+language%3APython&type=code


Expand All @@ -157,20 +157,20 @@ How to Teach This
Working with exception groups is an intermediate-to-advanced topic, unlikely
to arise for beginning programmers. We therefore suggest teaching this topic
via documentation, and via just-in-time feedback from static analysis tools.
In intermediate classes, we recommend teaching ``.flat_exceptions()`` together
In intermediate classes, we recommend teaching ``.leaf_exceptions()`` together
with the ``.split()`` and ``.subgroup()`` methods, and mentioning
``.preserve_context()`` as an advanced option to address specific pain points.

Both the API reference and the existing `ExceptionGroup tutorial`__
should be updated to demonstrate and explain the new methods. The tutorial
should include examples of common patterns where ``.flat_exceptions()`` and
should include examples of common patterns where ``.leaf_exceptions()`` and
``.preserve_context()`` help simplify error handling logic. Downstream
libraries which often use exception groups could include similar docs.

__ https://docs.python.org/3/tutorial/errors.html#raising-and-handling-multiple-unrelated-exceptions

We have also designed lint rules for inclusion in ``flake8-async`` which will
suggest using ``.flat_exceptions()`` when iterating over ``group.exceptions``
suggest using ``.leaf_exceptions()`` when iterating over ``group.exceptions``
or re-raising a leaf exception, and suggest using ``.preserve_context()`` when
re-raising a leaf exception inside an ``except*`` block would override any
existing context.
Expand All @@ -186,7 +186,7 @@ on older versions of Python, and can demonstrate the intended semantics.
We have found these helper functions quite useful when working with
:class:`ExceptionGroup`\ s in a large production codebase.

A ``flat_exceptions()`` helper function
A ``leaf_exceptions()`` helper function
---------------------------------------

.. code-block:: python
Expand All @@ -196,7 +196,7 @@ A ``flat_exceptions()`` helper function
from types import TracebackType


def flat_exceptions(
def leaf_exceptions(
self: BaseExceptionGroup, *, fix_traceback: bool = True
) -> list[BaseException]:
"""
Expand Down Expand Up @@ -297,11 +297,11 @@ Add ``BaseException.as_group()`` (or group methods)
Our survey of ``ExceptionGroup``-related error handling code also observed
many cases of duplicated logic to handle both a bare exception, and the same
kind of exception inside a group (often incorrectly, motivating
``.flat_exceptions()``).
``.leaf_exceptions()``).

We briefly `proposed <https://github.com/python/cpython/issues/125825>`__
adding ``.split(...)`` and ``.subgroup(...)`` methods too all exceptions,
before considering ``.flat_exceptions()`` made us feel this was too clumsy.
before considering ``.leaf_exceptions()`` made us feel this was too clumsy.
As a cleaner alternative, we sketched out an ``.as_group()`` method:

.. code-block:: python
Expand Down Expand Up @@ -329,6 +329,20 @@ less magical and tempting to use in cases where it would not be appropriate.
We could be argued around though, if others prefer this form.


Preserve additional attributes
------------------------------

We decided against preserving the ``__cause__`` and ``__suppress_context__``
attributes, because they are not changed by re-raising the exception, and we
prefer to support ``raise exc from None`` or ``raise exc from cause_exc``
together with ``with exc.preserve_context():``.

Similarly, we considered preserving the ``__traceback__`` attribute, and
decided against because the additional ``raise ...`` statement may be an
important clue when understanding some error. If end users wish to pop a
frame from the traceback, they can do with a separate context manager.


Footnotes
=========

Expand All @@ -337,7 +351,7 @@ Footnotes
<https://github.com/search?q=%2Ffor+%5Cw%2B+in+%5Beg%5D%5Cw*%5C.exceptions%3A%2F+language%3APython&type=code>`__
for ``for \w+ in [eg]\w*\.exceptions:``, we find:

* Four functions implementing ``flat_exceptions()`` semantics, none of
* Four functions implementing ``leaf_exceptions()`` semantics, none of
which preserve tracebacks:
(`one <https://github.com/nonebot/nonebot2/blob/570bd9587af99dd01a7d5421d3105d8a8e2aba32/nonebot/utils.py#L259-L266>`__,
`two <https://github.com/HypothesisWorks/hypothesis/blob/7c49f2daf602bc4e51161b6c0bc21720d64de9eb/hypothesis-python/src/hypothesis/core.py#L763-L770>`__,
Expand All @@ -354,7 +368,7 @@ Footnotes
`six <https://github.com/sobolevn/faststream/blob/0d6c9ee6b7703efab04387c51c72876e25ad91a7/faststream/app.py#L54-L56>`__)

* Seven cases which mishandle nested exception groups, and would thus
benefit from ``flat_exceptions()``. We were surprised to note that only
benefit from ``leaf_exceptions()``. We were surprised to note that only
one of these cases could straightforwardly be replaced by use of an
``except*`` clause or ``.subgroup()`` method.
(`one <https://github.com/vertexproject/synapse/blob/ed8148abb857d4445d727768d4c57f4f11b0d20a/synapse/lib/stormlib/iters.py#L82-L88>`__,
Expand All @@ -375,6 +389,14 @@ Footnotes
We expect that ``except*`` will be widely used in such cases by the time
that the methods proposed by this PEP are widely available.

.. [#naming]
The name ``leaf_exceptions()`` was `first proposed`__ in an early
precursor to :pep:`654`. If the prototype had matched ``except*``
in wrapping bare exceptions in a group, we might even have included
a ``.leaf_exceptions()`` method in that earlier PEP!

__ https://github.com/python-trio/exceptiongroup/pull/13


Copyright
=========
Expand Down