Skip to content

Commit 9925e70

Browse files
authored
bpo-45292: [PEP-654] exception groups and except* documentation (GH-30158)
1 parent 68c76d9 commit 9925e70

File tree

3 files changed

+208
-1
lines changed

3 files changed

+208
-1
lines changed

Doc/library/exceptions.rst

+72
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,78 @@ The following exceptions are used as warning categories; see the
851851
.. versionadded:: 3.2
852852

853853

854+
Exception groups
855+
----------------
856+
857+
The following are used when it is necessary to raise multiple unrelated
858+
exceptions. They are part of the exception hierarchy so they can be
859+
handled with :keyword:`except` like all other exceptions. In addition,
860+
they are recognised by :keyword:`except*<except_star>`, which matches
861+
their subgroups based on the types of the contained exceptions.
862+
863+
.. exception:: ExceptionGroup(msg, excs)
864+
.. exception:: BaseExceptionGroup(msg, excs)
865+
866+
Both of these exception types wrap the exceptions in the sequence ``excs``.
867+
The ``msg`` parameter must be a string. The difference between the two
868+
classes is that :exc:`BaseExceptionGroup` extends :exc:`BaseException` and
869+
it can wrap any exception, while :exc:`ExceptionGroup` extends :exc:`Exception`
870+
and it can only wrap subclasses of :exc:`Exception`. This design is so that
871+
``except Exception`` catches an :exc:`ExceptionGroup` but not
872+
:exc:`BaseExceptionGroup`.
873+
874+
The :exc:`BaseExceptionGroup` constructor returns an :exc:`ExceptionGroup`
875+
rather than a :exc:`BaseExceptionGroup` if all contained exceptions are
876+
:exc:`Exception` instances, so it can be used to make the selection
877+
automatic. The :exc:`ExceptionGroup` constructor, on the other hand,
878+
raises a :exc:`TypeError` if any contained exception is not an
879+
:exc:`Exception` subclass.
880+
881+
.. method:: subgroup(condition)
882+
883+
Returns an exception group that contains only the exceptions from the
884+
current group that match *condition*, or ``None`` if the result is empty.
885+
886+
The condition can be either a function that accepts an exception and returns
887+
true for those that should be in the subgroup, or it can be an exception type
888+
or a tuple of exception types, which is used to check for a match using the
889+
same check that is used in an ``except`` clause.
890+
891+
The nesting structure of the current exception is preserved in the result,
892+
as are the values of its :attr:`message`, :attr:`__traceback__`,
893+
:attr:`__cause__`, :attr:`__context__` and :attr:`__note__` fields.
894+
Empty nested groups are omitted from the result.
895+
896+
The condition is checked for all exceptions in the nested exception group,
897+
including the top-level and any nested exception groups. If the condition is
898+
true for such an exception group, it is included in the result in full.
899+
900+
.. method:: split(condition)
901+
902+
Like :meth:`subgroup`, but returns the pair ``(match, rest)`` where ``match``
903+
is ``subgroup(condition)`` and ``rest`` is the remaining non-matching
904+
part.
905+
906+
.. method:: derive(excs)
907+
908+
Returns an exception group with the same :attr:`message`,
909+
:attr:`__traceback__`, :attr:`__cause__`, :attr:`__context__`
910+
and :attr:`__note__` but which wraps the exceptions in ``excs``.
911+
912+
This method is used by :meth:`subgroup` and :meth:`split`. A
913+
subclass needs to override it in order to make :meth:`subgroup`
914+
and :meth:`split` return instances of the subclass rather
915+
than :exc:`ExceptionGroup`. ::
916+
917+
>>> class MyGroup(ExceptionGroup):
918+
... def derive(self, exc):
919+
... return MyGroup(self.message, exc)
920+
...
921+
>>> MyGroup("eg", [ValueError(1), TypeError(2)]).split(TypeError)
922+
(MyGroup('eg', [TypeError(2)]), MyGroup('eg', [ValueError(1)]))
923+
924+
.. versionadded:: 3.11
925+
854926

855927
Exception hierarchy
856928
-------------------

Doc/reference/compound_stmts.rst

+47-1
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ returns the list ``[0, 1, 2]``.
199199

200200
.. _try:
201201
.. _except:
202+
.. _except_star:
202203
.. _finally:
203204

204205
The :keyword:`!try` statement
@@ -216,12 +217,16 @@ The :keyword:`try` statement specifies exception handlers and/or cleanup code
216217
for a group of statements:
217218

218219
.. productionlist:: python-grammar
219-
try_stmt: `try1_stmt` | `try2_stmt`
220+
try_stmt: `try1_stmt` | `try2_stmt` | `try3_stmt`
220221
try1_stmt: "try" ":" `suite`
221222
: ("except" [`expression` ["as" `identifier`]] ":" `suite`)+
222223
: ["else" ":" `suite`]
223224
: ["finally" ":" `suite`]
224225
try2_stmt: "try" ":" `suite`
226+
: ("except" "*" `expression` ["as" `identifier`] ":" `suite`)+
227+
: ["else" ":" `suite`]
228+
: ["finally" ":" `suite`]
229+
try3_stmt: "try" ":" `suite`
225230
: "finally" ":" `suite`
226231

227232

@@ -304,6 +309,47 @@ when leaving an exception handler::
304309
>>> print(sys.exc_info())
305310
(None, None, None)
306311

312+
.. index::
313+
keyword: except_star
314+
315+
The :keyword:`except*<except_star>` clause(s) are used for handling
316+
:exc:`ExceptionGroup`s. The exception type for matching is interpreted as in
317+
the case of :keyword:`except`, but in the case of exception groups we can have
318+
partial matches when the type matches some of the exceptions in the group.
319+
This means that multiple except* clauses can execute, each handling part of
320+
the exception group. Each clause executes once and handles an exception group
321+
of all matching exceptions. Each exception in the group is handled by at most
322+
one except* clause, the first that matches it. ::
323+
324+
>>> try:
325+
... raise ExceptionGroup("eg",
326+
... [ValueError(1), TypeError(2), OSError(3), OSError(4)])
327+
... except* TypeError as e:
328+
... print(f'caught {type(e)} with nested {e.exceptions}')
329+
... except* OSError as e:
330+
... print(f'caught {type(e)} with nested {e.exceptions}')
331+
...
332+
caught <class 'ExceptionGroup'> with nested (TypeError(2),)
333+
caught <class 'ExceptionGroup'> with nested (OSError(3), OSError(4))
334+
+ Exception Group Traceback (most recent call last):
335+
| File "<stdin>", line 2, in <module>
336+
| ExceptionGroup: eg
337+
+-+---------------- 1 ----------------
338+
| ValueError: 1
339+
+------------------------------------
340+
>>>
341+
342+
Any remaining exceptions that were not handled by any except* clause
343+
are re-raised at the end, combined into an exception group along with
344+
all exceptions that were raised from within except* clauses.
345+
346+
An except* clause must have a matching type, and this type cannot be a
347+
subclass of :exc:`BaseExceptionGroup`. It is not possible to mix except
348+
and except* in the same :keyword:`try`. :keyword:`break`,
349+
:keyword:`continue` and :keyword:`return` cannot appear in an except*
350+
clause.
351+
352+
307353
.. index::
308354
keyword: else
309355
statement: return

Doc/tutorial/errors.rst

+89
Original file line numberDiff line numberDiff line change
@@ -462,3 +462,92 @@ used in a way that ensures they are always cleaned up promptly and correctly. ::
462462
After the statement is executed, the file *f* is always closed, even if a
463463
problem was encountered while processing the lines. Objects which, like files,
464464
provide predefined clean-up actions will indicate this in their documentation.
465+
466+
467+
.. _tut-exception-groups:
468+
469+
Raising and Handling Multiple Unrelated Exceptions
470+
==================================================
471+
472+
There are situations where it is necessary to report several exceptions that
473+
have occurred. This it often the case in concurrency frameworks, when several
474+
tasks may have failed in parallel, but there are also other use cases where
475+
it is desirable to continue execution and collect multiple errors rather than
476+
raise the first exception.
477+
478+
The builtin :exc:`ExceptionGroup` wraps a list of exception instances so
479+
that they can be raised together. It is an exception itself, so it can be
480+
caught like any other exception. ::
481+
482+
>>> def f():
483+
... excs = [OSError('error 1'), SystemError('error 2')]
484+
... raise ExceptionGroup('there were problems', excs)
485+
...
486+
>>> f()
487+
+ Exception Group Traceback (most recent call last):
488+
| File "<stdin>", line 1, in <module>
489+
| File "<stdin>", line 3, in f
490+
| ExceptionGroup: there were problems
491+
+-+---------------- 1 ----------------
492+
| OSError: error 1
493+
+---------------- 2 ----------------
494+
| SystemError: error 2
495+
+------------------------------------
496+
>>> try:
497+
... f()
498+
... except Exception as e:
499+
... print(f'caught {type(e)}: e')
500+
...
501+
caught <class 'ExceptionGroup'>: e
502+
>>>
503+
504+
By using ``except*`` instead of ``except``, we can selectively
505+
handle only the exceptions in the group that match a certain
506+
type. In the following example, which shows a nested exception
507+
group, each ``except*`` clause extracts from the group exceptions
508+
of a certain type while letting all other exceptions propagate to
509+
other clauses and eventually to be reraised. ::
510+
511+
>>> def f():
512+
... raise ExceptionGroup("group1",
513+
... [OSError(1),
514+
... SystemError(2),
515+
... ExceptionGroup("group2",
516+
... [OSError(3), RecursionError(4)])])
517+
...
518+
>>> try:
519+
... f()
520+
... except* OSError as e:
521+
... print("There were OSErrors")
522+
... except* SystemError as e:
523+
... print("There were SystemErrors")
524+
...
525+
There were OSErrors
526+
There were SystemErrors
527+
+ Exception Group Traceback (most recent call last):
528+
| File "<stdin>", line 2, in <module>
529+
| File "<stdin>", line 2, in f
530+
| ExceptionGroup: group1
531+
+-+---------------- 1 ----------------
532+
| ExceptionGroup: group2
533+
+-+---------------- 1 ----------------
534+
| RecursionError: 4
535+
+------------------------------------
536+
>>>
537+
538+
Note that the exceptions nested in an exception group must be instances,
539+
not types. This is because in practice the exceptions would typically
540+
be ones that have already been raised and caught by the program, along
541+
the following pattern::
542+
543+
>>> excs = []
544+
... for test in tests:
545+
... try:
546+
... test.run()
547+
... except Exception as e:
548+
... excs.append(e)
549+
...
550+
>>> if excs:
551+
... raise ExceptionGroup("Test Failures", excs)
552+
...
553+

0 commit comments

Comments
 (0)