Skip to content

Commit bdc8b83

Browse files
authored
PEP 785: rename method, and a rejected idea (#4376)
* PEP 785: why not more attributes * PEP 785: rename to 'leaf_exceptions()'
1 parent 1bd750c commit bdc8b83

File tree

1 file changed

+40
-18
lines changed

1 file changed

+40
-18
lines changed

peps/pep-0785.rst

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ As :pep:`654` :class:`ExceptionGroup` has come into widespread use across the
1717
Python community, some common but awkward patterns have emerged. We therefore
1818
propose adding two new methods to exception objects:
1919

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

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

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

@@ -55,7 +55,7 @@ unwrap ``HTTPException`` if that is the sole leaf of a group:
5555
.. code-block:: python
5656
5757
except* HTTPException as group:
58-
first, *rest = group.flat_exceptions() # get the whole traceback :-)
58+
first, *rest = group.leaf_exceptions() # get the whole traceback :-)
5959
if not rest:
6060
raise first
6161
raise
@@ -78,12 +78,12 @@ readable, and easy-to-use solution for these cases.
7878
Specification
7979
=============
8080

81-
A new method ``flat_exceptions()`` will be added to ``BaseExceptionGroup``, with the
81+
A new method ``leaf_exceptions()`` will be added to ``BaseExceptionGroup``, with the
8282
following signature:
8383

8484
.. code-block:: python
8585
86-
def flat_exceptions(self, *, fix_tracebacks=True) -> list[BaseException]:
86+
def leaf_exceptions(self, *, fix_tracebacks=True) -> list[BaseException]:
8787
"""
8888
Return a flat list of all 'leaf' exceptions in the group.
8989
@@ -118,7 +118,7 @@ Usage example:
118118
try:
119119
user_code_here()
120120
except* HTTPException as group:
121-
first, *rest = group.flat_exceptions()
121+
first, *rest = group.leaf_exceptions()
122122
if rest:
123123
raise # handled by internal-server-error middleware
124124
... # logging, cache updates, etc.
@@ -142,12 +142,12 @@ Backwards Compatibility
142142

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

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

153153

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

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

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

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

189-
A ``flat_exceptions()`` helper function
189+
A ``leaf_exceptions()`` helper function
190190
---------------------------------------
191191

192192
.. code-block:: python
@@ -196,7 +196,7 @@ A ``flat_exceptions()`` helper function
196196
from types import TracebackType
197197
198198
199-
def flat_exceptions(
199+
def leaf_exceptions(
200200
self: BaseExceptionGroup, *, fix_traceback: bool = True
201201
) -> list[BaseException]:
202202
"""
@@ -297,11 +297,11 @@ Add ``BaseException.as_group()`` (or group methods)
297297
Our survey of ``ExceptionGroup``-related error handling code also observed
298298
many cases of duplicated logic to handle both a bare exception, and the same
299299
kind of exception inside a group (often incorrectly, motivating
300-
``.flat_exceptions()``).
300+
``.leaf_exceptions()``).
301301

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

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

331331

332+
Preserve additional attributes
333+
------------------------------
334+
335+
We decided against preserving the ``__cause__`` and ``__suppress_context__``
336+
attributes, because they are not changed by re-raising the exception, and we
337+
prefer to support ``raise exc from None`` or ``raise exc from cause_exc``
338+
together with ``with exc.preserve_context():``.
339+
340+
Similarly, we considered preserving the ``__traceback__`` attribute, and
341+
decided against because the additional ``raise ...`` statement may be an
342+
important clue when understanding some error. If end users wish to pop a
343+
frame from the traceback, they can do with a separate context manager.
344+
345+
332346
Footnotes
333347
=========
334348

@@ -337,7 +351,7 @@ Footnotes
337351
<https://github.com/search?q=%2Ffor+%5Cw%2B+in+%5Beg%5D%5Cw*%5C.exceptions%3A%2F+language%3APython&type=code>`__
338352
for ``for \w+ in [eg]\w*\.exceptions:``, we find:
339353
340-
* Four functions implementing ``flat_exceptions()`` semantics, none of
354+
* Four functions implementing ``leaf_exceptions()`` semantics, none of
341355
which preserve tracebacks:
342356
(`one <https://github.com/nonebot/nonebot2/blob/570bd9587af99dd01a7d5421d3105d8a8e2aba32/nonebot/utils.py#L259-L266>`__,
343357
`two <https://github.com/HypothesisWorks/hypothesis/blob/7c49f2daf602bc4e51161b6c0bc21720d64de9eb/hypothesis-python/src/hypothesis/core.py#L763-L770>`__,
@@ -354,7 +368,7 @@ Footnotes
354368
`six <https://github.com/sobolevn/faststream/blob/0d6c9ee6b7703efab04387c51c72876e25ad91a7/faststream/app.py#L54-L56>`__)
355369
356370
* Seven cases which mishandle nested exception groups, and would thus
357-
benefit from ``flat_exceptions()``. We were surprised to note that only
371+
benefit from ``leaf_exceptions()``. We were surprised to note that only
358372
one of these cases could straightforwardly be replaced by use of an
359373
``except*`` clause or ``.subgroup()`` method.
360374
(`one <https://github.com/vertexproject/synapse/blob/ed8148abb857d4445d727768d4c57f4f11b0d20a/synapse/lib/stormlib/iters.py#L82-L88>`__,
@@ -375,6 +389,14 @@ Footnotes
375389
We expect that ``except*`` will be widely used in such cases by the time
376390
that the methods proposed by this PEP are widely available.
377391
392+
.. [#naming]
393+
The name ``leaf_exceptions()`` was `first proposed`__ in an early
394+
precursor to :pep:`654`. If the prototype had matched ``except*``
395+
in wrapping bare exceptions in a group, we might even have included
396+
a ``.leaf_exceptions()`` method in that earlier PEP!
397+
398+
__ https://github.com/python-trio/exceptiongroup/pull/13
399+
378400

379401
Copyright
380402
=========

0 commit comments

Comments
 (0)