Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-45292: [PEP-654] exception groups and except* documentation #30158

Merged
merged 17 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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
62 changes: 62 additions & 0 deletions Doc/library/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,68 @@ The following exceptions are used as warning categories; see the
.. versionadded:: 3.2


Exception groups
----------------

The following are used when it is necessary to raise multiple unrelated
exceptions. They are part of the exception hierarchy so they can be
handled with :keyword:`except` like all other exceptions. In addition,
they are recognised by :keyword:`except*<except_star>`, which matches
their subgroups based on the types of the contained exceptions.

.. exception:: ExceptionGroup(msg, excs)
.. exception:: BaseExceptionGroup(msg, excs)

Both of these exception types wrap the exceptions in the sequence ``excs``.
The ``msg`` parameter must be a string. The difference between the two
classes is that :exc:`BaseExceptionGroup` extends :exc:`BaseException` and
it can wrap any exception, while :exc:`ExceptionGroup` extends :exc:`Exception`
and it can only wrap subclasses of :exc:`Exception`. This design is so that
``except Exception`` catches an :exc:`ExceptionGroup` but not
:exc:`BaseExceptionGroup`.

It is usually not necessary for a program to explicitly create a
:exc:`BaseExceptionGroup`, because the :exc:`ExceptionGroup` constructor
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
inspects the contained exceptions, and if any of them are not of type
:exc:`Exception` it returns a :exc:`BaseExceptionGroup` rather than an
:exc:`ExceptionGroup`. However, this behavior is not automatically true for
subclasses of :exc:`ExceptionGroup`.

.. method:: subgroup(condition)

Returns an exception group that contains only the exceptions from the
current group that match *condition*, or ``None`` if the result is empty.

The condition can be either a function that accepts an exception and returns
true for those that should be in the subgroup, or it can be an exception type
or a tuple of exception types, which is used to check for a match using the
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
same check that is used in an ``except`` clause.

The nesting structure of the current exception is preserved in the result,
as are the values of its ``msg``, ``__traceback__``, ``__cause__``,
``__context__`` and ``__note__`` fields. Empty nested groups are omitted
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
from the result.

The condition is checked for all exceptions in the nested exception group,
including the top-level and any nested exception groups. If the condition is
true for such an exception group, it is included in the result in full.

.. method:: split(condition)

Like :meth:`subgroup`, but returns the pair ``(match, rest)`` where ``match``
is ``subgroup(condition)`` and ``rest`` is the remainder, the non-matching
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
part.

.. method:: derive(excs)

Returns an exception group with the same ``msg``, ``__traceback__``,
``__cause__``, ``__context__`` and ``__note__`` but which wraps the
exceptions in ``excs``. This method is used by :meth:`subgroup` and
:meth:`split` and may need to be overridden in subclasses if there are
additional values that need to be copied over to the result.

.. versionadded:: 3.11


Exception hierarchy
-------------------
Expand Down
6 changes: 6 additions & 0 deletions Doc/library/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ capture data for later printing in a lightweight fashion.

The ``__suppress_context__`` value from the original exception.

.. attribute:: __note__

The ``__note__`` value from the original exception.
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 3.11

.. attribute:: stack

A :class:`StackSummary` representing the traceback.
Expand Down
48 changes: 47 additions & 1 deletion Doc/reference/compound_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ returns the list ``[0, 1, 2]``.

.. _try:
.. _except:
.. _except_star:
.. _finally:

The :keyword:`!try` statement
Expand All @@ -237,12 +238,16 @@ The :keyword:`try` statement specifies exception handlers and/or cleanup code
for a group of statements:

.. productionlist:: python-grammar
try_stmt: `try1_stmt` | `try2_stmt`
try_stmt: `try1_stmt` | `try2_stmt` | `try3_stmt`
try1_stmt: "try" ":" `suite`
: ("except" [`expression` ["as" `identifier`]] ":" `suite`)+
: ["else" ":" `suite`]
: ["finally" ":" `suite`]
try2_stmt: "try" ":" `suite`
: ("except" "*" `expression` ["as" `identifier`] ":" `suite`)+
: ["else" ":" `suite`]
: ["finally" ":" `suite`]
try3_stmt: "try" ":" `suite`
: "finally" ":" `suite`


Expand Down Expand Up @@ -325,6 +330,47 @@ when leaving an exception handler::
>>> print(sys.exc_info())
(None, None, None)

.. index::
keyword: except_star

The :keyword:`except*<except_star>` clause(s) are used for handling
:exc:`ExceptionGroup`s. The exception type for matching is interpreted as in
the case of :keyword:`except`, but in the case of exception groups we can have
partial matches when the type matches some of the exceptions in the group.
This means that multiple except* clauses can execute, each handling part of
the exception group. Each clause executes once and handles an exception group
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
of all matching exceptions. Each exception in the group is handled by at most
one except* clause, the first that matches it. ::
Fidget-Spinner marked this conversation as resolved.
Show resolved Hide resolved

>>> try:
... raise ExceptionGroup("eg",
... [ValueError(1), TypeError(2), OSError(3), OSError(4)])
... except* TypeError as e:
... print(f'caught {type(e)} with nested {e.exceptions}')
... except* OSError as e:
... print(f'caught {type(e)} with nested {e.exceptions}')
...
caught <class 'ExceptionGroup'> with nested (TypeError(2),)
caught <class 'ExceptionGroup'> with nested (OSError(3), OSError(4))
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| ExceptionGroup: eg
+-+---------------- 1 ----------------
| ValueError: 1
+------------------------------------
>>>

Any remaining exceptions that were not handled by any except* clause
are re-raised at the end, combined into an exception group along with
all exceptions that were raised from within except* clauses.

An except* clause must have a matching type, and this type cannot be a
subclass of :exc:`BaseExceptionGroup`. It is not possible to mix except
and except* in the same :keyword:`try`. :keyword:`break`,
:keyword:`continue` and :keyword:`return` cannot appear in an except*
clause.


.. index::
keyword: else
statement: return
Expand Down
150 changes: 150 additions & 0 deletions Doc/tutorial/errors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,153 @@ used in a way that ensures they are always cleaned up promptly and correctly. ::
After the statement is executed, the file *f* is always closed, even if a
problem was encountered while processing the lines. Objects which, like files,
provide predefined clean-up actions will indicate this in their documentation.


.. _tut-exception-groups:

Raising and Handling Multiple Unrelated Exceptions
==================================================

There are situations where it is necessary to report several exceptions that
have occurred. This it often the case in concurrency frameworks, when several
tasks may have failed in parallel, but there are also other use cases where
it is desirable to continue execution and collect multiple errors rather than
raise the first exception.

The builtin :exc:`ExceptionGroup` wraps a list of exception instances so
that they can be raised together. It is an exception itself, so it can be
caught like any other exception. ::

>>> def f():
... excs = [OSError('error 1'), SystemError('error 2')]
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
... raise ExceptionGroup('there were problems', excs)
...
>>> f()
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| File "<stdin>", line 3, in f
| ExceptionGroup: there were problems
+-+---------------- 1 ----------------
| OSError: error 1
+---------------- 2 ----------------
| SystemError: error 2
+------------------------------------
>>> try:
... f()
... except Exception as e:
... print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
>>>

By using ``except*`` instead of ``except``, we can selectively
handle only the exceptions in the group that match a certain
type. In the following example, which shows a nested exception
group, each ``except*`` clause extracts from the group exceptions
of a certain type while letting all other exceptions propagate to
other clauses and eventually to be reraised. ::

>>> def f():
... raise ExceptionGroup("group1",
... [OSError(1),
... SystemError(2),
... ExceptionGroup("group2",
... [OSError(3), RecursionError(4)])])
...
>>> try:
... f()
... except* OSError as e:
... print("There were OSErrors")
... except* SystemError as e:
... print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 2, in <module>
| File "<stdin>", line 2, in f
| ExceptionGroup: group1
+-+---------------- 1 ----------------
| ExceptionGroup: group2
+-+---------------- 1 ----------------
| RecursionError: 4
+------------------------------------
>>>

Note that the exceptions nested in an exception group must be instances,
not types. This is because in practice the exceptions would typically
be ones that have already been raised and caught by the program, along
the following pattern::

>>> excs = []
... for test in tests:
... try:
... test.run()
... except Exception as e:
... excs.append(e)
...
... if excs:
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
... raise ExceptionGroup("Test Failures", excs)
...


Enriching Exceptions with Notes
===============================

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 this
purpose, exceptions have a mutable field ``__note__`` that can be assigned to
a string which is included in formatted tracebacks. ::

>>> 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
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved
>>>

For example, when collecting exceptions into an exception group, we may want
to add context information for the individual errors. In the following each
exception in the group has a note indicating when this error has occurred. ::

>>> def f():
... raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.__note__ = f'Happened in Iteration {i+1}'
... excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| ExceptionGroup: We have some problems
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| File "<stdin>", line 2, in f
| OSError: operation failed
| Happened in Iteration 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| File "<stdin>", line 2, in f
| OSError: operation failed
| Happened in Iteration 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| File "<stdin>", line 2, in f
| OSError: operation failed
| Happened in Iteration 3
+------------------------------------
>>>