PEP 654: ExceptionGroup --> exception group in prose (#1855)
This commit is contained in:
parent
7d760822a3
commit
a3f0183936
238
pep-0654.rst
238
pep-0654.rst
|
@ -101,10 +101,10 @@ errors that had occurred, extract the ones it wants to handle, and reraise the
|
|||
rest.
|
||||
|
||||
Changes to the language are required in order to extend support for
|
||||
``ExceptionGroups`` in the style of existing exception handling mechanisms. At
|
||||
the very least we would like to be able to catch an ``ExceptionGroup`` only if
|
||||
it contains an exception type that we choose to handle. Exceptions of
|
||||
other types in the same ``ExceptionGroup`` need to be automatically reraised,
|
||||
exception groups in the style of existing exception handling mechanisms. At
|
||||
the very least we would like to be able to catch an exception group only if
|
||||
it contains an exception of a type that we choose to handle. Exceptions of
|
||||
other types in the same group need to be automatically reraised,
|
||||
otherwise it is too easy for user code to inadvertently swallow exceptions
|
||||
that it is not handling.
|
||||
|
||||
|
@ -113,16 +113,16 @@ for this purpose, in a backwards-compatible manner, and found that it is not.
|
|||
See the `Rejected Ideas`_ section for more on this.
|
||||
|
||||
The purpose of this PEP, then, is to add the ``except*`` syntax for handling
|
||||
``ExceptionGroups`` in the interpreter, which in turn requires that
|
||||
``ExceptionGroup`` is added as a builtin type. The semantics of handling
|
||||
``ExceptionGroups`` are not backwards compatible with the current exception
|
||||
handling semantics, so we are not proposing to modify the behavior of the
|
||||
exception groups in the interpreter, which in turn requires that
|
||||
``ExceptionGroup`` is added as a builtin type. The desired semantics of
|
||||
``except*`` are sufficiently different from the current exception
|
||||
handling semantics that we are not proposing to modify the behavior of the
|
||||
``except`` keyword but rather to add the new ``except*`` syntax.
|
||||
|
||||
Our premise is that ``ExceptionGroups`` and ``except*`` will be used
|
||||
Our premise is that exception groups and ``except*`` will be used
|
||||
selectively, only when they are needed. We do not expect them to become
|
||||
the default mechanism for exception handling. The decision to raise
|
||||
``ExceptionGroup`` from a library needs to be considered carefully and
|
||||
exception groups from a library needs to be considered carefully and
|
||||
regarded as an API-breaking change. We expect that this will normally be
|
||||
done by introducing a new API rather than modifying an existing one.
|
||||
|
||||
|
@ -130,8 +130,8 @@ done by introducing a new API rather than modifying an existing one.
|
|||
Specification
|
||||
=============
|
||||
|
||||
ExceptionGroup
|
||||
--------------
|
||||
ExceptionGroup and BaseExceptionGroup
|
||||
-------------------------------------
|
||||
|
||||
We propose to add two new builtin exception types:
|
||||
``BaseExceptionGroup(BaseException)`` and
|
||||
|
@ -141,27 +141,29 @@ raised and handled as any exception with ``raise ExceptionGroup(...)`` and
|
|||
``try: ... except ExceptionGroup: ...`` or ``raise BaseExceptionGroup(...)``
|
||||
and ``try: ... except BaseExceptionGroup: ...``.
|
||||
|
||||
Both have a constructor that takes two positional-only parameters: a message
|
||||
string and a sequence of the nested exceptions, for example:
|
||||
Both have a constructor that takes two positional-only arguments: a message
|
||||
string and a sequence of the nested exceptions, which are exposed in the
|
||||
fields ``message`` and ``errors``. For example:
|
||||
``ExceptionGroup('issues', [ValueError('bad value'), TypeError('bad type')])``.
|
||||
The difference between them is that ``ExceptionGroup`` can only wrap
|
||||
``Exception`` subclasses while ``BaseExceptionGroup`` can wrap any
|
||||
``BaseException`` subclass. A factory method that inspects the nested
|
||||
execptions and selects between ``ExceptionGroup`` and ``BaseExceptionGroup``
|
||||
makes the choice automatic. In the rest of the document, unless stated
|
||||
otherwise, when we refer to ``ExceptionGroup`` we mean either an
|
||||
``ExceptionGroup`` or a ``BaseExceptionGroup``.
|
||||
makes the choice automatic. In the rest of the document, when we refer to
|
||||
an exception group, we mean either an ``ExceptionGroup`` or a
|
||||
``BaseExceptionGroup``. When it is necessary to make the distunction, we
|
||||
use the class name. For brevity, we will use ``ExceptionGroup`` in code
|
||||
examples that are relevant to both.
|
||||
|
||||
The ``ExceptionGroup`` class exposes these parameters in the fields ``message``
|
||||
and ``errors``. A nested exception can also be an ``ExceptionGroup`` so the
|
||||
class represents a tree of exceptions, where the leaves are plain exceptions and
|
||||
each internal node represent a time at which the program grouped some
|
||||
unrelated exceptions into a new ``ExceptionGroup`` and raised them together.
|
||||
The ``ExceptionGroup`` class is final, i.e., it cannot be subclassed.
|
||||
Since an exception group can be nested, it represents a tree of exceptions,
|
||||
where the leaves are plain exceptions and each internal node represent a time
|
||||
at which the program grouped some unrelated exceptions into a new group and
|
||||
raised them together. The ``ExceptionGroup`` and ``BaseExceptionGroup``
|
||||
classes are final, i.e., they cannot be further subclassed.
|
||||
|
||||
The ``ExceptionGroup.subgroup(condition)`` method gives us a way to obtain an
|
||||
``ExceptionGroup`` that has the same metadata (cause, context, traceback) as
|
||||
the original group, and the same nested structure of ``ExceptionGroups``, but
|
||||
The ``BaseExceptionGroup.subgroup(condition)`` method gives us a way to obtain
|
||||
an exception group that has the same metadata (cause, context, traceback) as
|
||||
the original group, and the same nested structure of groups, but
|
||||
contains only those exceptions for which the condition is true:
|
||||
|
||||
.. code-block::
|
||||
|
@ -206,20 +208,21 @@ contains only those exceptions for which the condition is true:
|
|||
>>>
|
||||
|
||||
|
||||
Empty nested ``ExceptionGroups`` are omitted from the result, as in the
|
||||
Empty nested groups are omitted from the result, as in the
|
||||
case of ``ExceptionGroup("three")`` in the example above. If none of the
|
||||
leaf exceptions match the condition, ``subgroup`` returns ``None`` rather
|
||||
than an empty ``ExceptionGroup``. The original ``eg``
|
||||
than an empty group. The original ``eg``
|
||||
is unchanged by ``subgroup``, but the value returned is not necessarily a full
|
||||
new copy. Leaf exceptions are not copied, nor are ``ExceptionGroups`` which are
|
||||
fully contained in the result. When it is necessary to partition an
|
||||
``ExceptionGroup`` because the condition holds for some, but not all of its
|
||||
contained exceptions, a new ``ExceptionGroup`` is created but the ``__cause__``,
|
||||
``__context__`` and ``__traceback__`` fields are copied by reference, so are
|
||||
shared with the original ``eg``.
|
||||
new copy. Leaf exceptions are not copied, nor are exception groups which are
|
||||
fully contained in the result. When it is necessary to partition a
|
||||
group because the condition holds for some, but not all of its
|
||||
contained exceptions, a new ``ExceptionGroup`` or ``BaseExceptionGroup``
|
||||
instance is created, while the ``__cause__``, ``__context__`` and
|
||||
``__traceback__`` fields are copied by reference, so they are shared with
|
||||
the original ``eg``.
|
||||
|
||||
If both the subgroup and its complement are needed, the
|
||||
``ExceptionGroup.split(condition)`` method can be used:
|
||||
``BaseExceptionGroup.split(condition)`` method can be used:
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
@ -264,8 +267,8 @@ as a shorthand for matching that type: ``eg.split(T)`` divides ``eg`` into the
|
|||
subgroup of leaf exceptions that match the type ``T``, and the subgroup of those
|
||||
that do not (using the same check as ``except`` for a match).
|
||||
|
||||
The Traceback of an ``ExceptionGroup``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
The Traceback of an Exception Group
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For regular exceptions, the traceback represents a simple path of frames,
|
||||
from the frame in which the exception was raised to the frame in which it
|
||||
|
@ -280,9 +283,9 @@ traceback's frame list is immutable in the sense that frames only need to be
|
|||
added at the head, and never need to be removed.
|
||||
|
||||
We do not need to make any changes to this data structure. The ``__traceback__``
|
||||
field of the ``ExceptionGroup`` instance represents the path that the contained
|
||||
field of the exception group instance represents the path that the contained
|
||||
exceptions travelled through together after being joined into the
|
||||
``ExceptionGroup``, and the same field on each of the nested exceptions
|
||||
group, and the same field on each of the nested exceptions
|
||||
represents the path through which this exception arrived at the frame of the
|
||||
merge.
|
||||
|
||||
|
@ -327,19 +330,19 @@ in the following example:
|
|||
ValueError: 1
|
||||
>>>
|
||||
|
||||
Handling ``ExceptionGroups``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Handling Exception Groups
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We expect that when programs catch and handle ``ExceptionGroups``, they will
|
||||
We expect that when programs catch and handle exception groups, they will
|
||||
typically either query to check if it has leaf exceptions for which some
|
||||
condition holds (using ``subgroup`` or ``split``) or format the exception
|
||||
(using the ``traceback`` module's methods).
|
||||
|
||||
It is less likely to be useful to iterate over the individual leaf exceptions.
|
||||
To see why, suppose that an application caught an ``ExceptionGroup`` raised in
|
||||
To see why, suppose that an application caught an exception group raised by
|
||||
an ``asyncio.gather()`` call. At this stage, the context for each specific
|
||||
exception is lost. Any recovery for this exception should have been performed
|
||||
before it was grouped with other exceptions into the ``ExceptionGroup`` [10]_.
|
||||
before it was grouped with other exceptions [10]_.
|
||||
Furthermore, the application is likely to react in the same way to any number
|
||||
of instances of a certain exception type, so it is more likely that we will
|
||||
want to know whether ``eg.subgroup(T)`` is None or not, than we are to be
|
||||
|
@ -347,7 +350,7 @@ interested in the number of ``Ts`` in ``eg``.
|
|||
|
||||
However, there are situations where it is necessary to inspect the
|
||||
individual leaf exceptions. For example, suppose that we have an
|
||||
``ExceptionGroup`` ``eg`` and we want to log the ``OSErrors`` that have a
|
||||
exception group ``eg`` and that we want to log the ``OSErrors`` that have a
|
||||
specific error code and reraise everything else. We can do this by passing
|
||||
a function with side effects to ``subgroup``, as follows:
|
||||
|
||||
|
@ -461,8 +464,8 @@ exceptions can be handled by each ``except*`` clause:
|
|||
In a traditional ``try-except`` statement there is only one exception to handle,
|
||||
so the body of at most one ``except`` clause executes; the first one that matches
|
||||
the exception. With the new syntax, an ``except*`` clause can match a subgroup
|
||||
of the ``ExceptionGroup`` that was raised, while the remaining part is matched
|
||||
by following ``except*`` clauses. In other words, a single ``ExceptionGroup`` can
|
||||
of the exception group that was raised, while the remaining part is matched
|
||||
by following ``except*`` clauses. In other words, a single exception group can
|
||||
cause several ``except*`` clauses to execute, but each such clause executes at
|
||||
most once (for all matching exceptions from the group) and each exception is
|
||||
either handled by exactly one clause (the first one that matches its type)
|
||||
|
@ -471,7 +474,7 @@ or is reraised at the end.
|
|||
For example, suppose that the body of the ``try`` block above raises
|
||||
``eg = ExceptionGroup('msg', [FooError(1), FooError(2), BazError()])``.
|
||||
The ``except*`` clauses are evaluated in order by calling ``split`` on the
|
||||
``unhandled`` ``ExceptionGroup``, which is initially equal to ``eg`` and then shrinks
|
||||
``unhandled`` exception group, which is initially equal to ``eg`` and then shrinks
|
||||
as exceptions are matched and extracted from it. In the first ``except*`` clause,
|
||||
``unhandled.split(SpamError)`` returns ``(None, unhandled)`` so the body of this
|
||||
block is not executed and ``unhandled`` is unchanged. For the second block,
|
||||
|
@ -521,8 +524,8 @@ The order of ``except*`` clauses is significant just like with the regular
|
|||
Recursive Matching
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The matching of ``except*`` clauses against an ``ExceptionGroup`` is performed
|
||||
recursively, using the ``ExceptionGroup.split()`` method:
|
||||
The matching of ``except*`` clauses against an exception group is performed
|
||||
recursively, using the ``split()`` method:
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
@ -549,8 +552,8 @@ recursively, using the ``ExceptionGroup.split()`` method:
|
|||
Unmatched Exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If not all exceptions in an ``ExceptionGroup`` were matched by the ``except*``
|
||||
clauses, the remaining part of the ``ExceptionGroup`` is propagated on:
|
||||
If not all exceptions in an exception group were matched by the ``except*``
|
||||
clauses, the remaining part of the group is propagated on:
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
@ -578,10 +581,11 @@ clauses, the remaining part of the ``ExceptionGroup`` is propagated on:
|
|||
Naked Exceptions
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
If the exception raised inside the ``try`` body is not of type ``ExceptionGroup``,
|
||||
we call it a ``naked`` exception. If its type matches one of the ``except*``
|
||||
clauses, it is caught and wrapped by an ``ExceptionGroup`` with an empty message
|
||||
string. This is to make the type of ``e`` consistent and statically known:
|
||||
If the exception raised inside the ``try`` body is not of type ``ExceptionGroup``
|
||||
or ``BaseExceptionGroup``, we call it a ``naked`` exception. If its type matches
|
||||
one of the ``except*`` clauses, it is caught and wrapped by an ``ExceptionGroup``
|
||||
(or ``BaseExceptionGroup`` if it is not an ``Exception`` subclass) with an empty
|
||||
message string. This is to make the type of ``e`` consistent and statically known:
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
@ -637,20 +641,20 @@ the stack:
|
|||
ZeroDivisionError: division by zero |
|
||||
|
||||
|
||||
This holds for ``ExceptionGroups`` as well, but the situation is now more complex
|
||||
This holds for exception groups as well, but the situation is now more complex
|
||||
because there can be exceptions raised and reraised from multiple ``except*``
|
||||
clauses, as well as unhandled exceptions that need to propagate.
|
||||
The interpreter needs to combine all those exceptions into a result, and
|
||||
raise that.
|
||||
|
||||
The reraised exceptions and the unhandled exceptions are subgroups of the
|
||||
original ``ExceptionGroup``, and share its metadata (cause, context, traceback).
|
||||
original group, and share its metadata (cause, context, traceback).
|
||||
On the other hand, each of the explicitly raised exceptions has its own
|
||||
metadata - the traceback contains the line from which it was raised, its
|
||||
cause is whatever it may have been explicitly chained to, and its context is the
|
||||
value of ``sys.exc_info()`` in the ``except*`` clause of the raise.
|
||||
|
||||
In the aggregated ``ExceptionGroup``, the reraised and unhandled exceptions have
|
||||
In the aggregated exception group, the reraised and unhandled exceptions have
|
||||
the same relative structure as in the original exception, as if they were split
|
||||
off together in one ``subgroup`` call. For example, in the snippet below the
|
||||
inner ``try-except*`` block raises an ``ExceptionGroup`` that contains all
|
||||
|
@ -688,8 +692,9 @@ the original ``ExceptionGroup``:
|
|||
|
||||
When exceptions are raised explicitly, they are independent of the original
|
||||
exception group, and cannot be merged with it (they have their own cause,
|
||||
context and traceback). Instead, they are combined into a new ``ExceptionGroup``,
|
||||
which also contains the reraised/unhandled subgroup described above.
|
||||
context and traceback). Instead, they are combined into a new ``ExceptionGroup``
|
||||
(or ``BaseExceptionGroup``), which also contains the reraised/unhandled
|
||||
subgroup described above.
|
||||
|
||||
In the following example, the ``ValueErrors`` were raised so they are in their
|
||||
own ``ExceptionGroup``, while the ``OSErrors`` were reraised so they were
|
||||
|
@ -755,7 +760,7 @@ merged with the unhandled ``TypeErrors``.
|
|||
Chaining
|
||||
~~~~~~~~
|
||||
|
||||
Explicitly raised ``ExceptionGroups`` are chained as with any exceptions. The
|
||||
Explicitly raised exception groups are chained as with any exceptions. The
|
||||
following example shows how part of ``ExceptionGroup`` "one" became the
|
||||
context for ``ExceptionGroup`` "two", while the other part was combined with
|
||||
it into the new ``ExceptionGroup``.
|
||||
|
@ -866,9 +871,9 @@ other clauses from the same ``try`` statement:
|
|||
|
||||
|
||||
Raising a new instance of a naked exception does not cause this exception to
|
||||
be wrapped by an ``ExceptionGroup``. Rather, the exception is raised as is, and
|
||||
be wrapped by an exception group. Rather, the exception is raised as is, and
|
||||
if it needs to be combined with other propagated exceptions, it becomes a
|
||||
direct child of the new ``ExceptionGroup`` created for that:
|
||||
direct child of the new exception group created for that:
|
||||
|
||||
|
||||
.. code-block::
|
||||
|
@ -930,9 +935,9 @@ direct child of the new ``ExceptionGroup`` created for that:
|
|||
>>>
|
||||
|
||||
|
||||
Finally, as an example of how the proposed API can help us work effectively
|
||||
with ``ExceptionGroups``, the following code ignores all ``EPIPE`` OS errors,
|
||||
while letting all other exceptions propagate.
|
||||
Finally, as an example of how the proposed semantics can help us work
|
||||
effectively with exception groups, the following code ignores all ``EPIPE``
|
||||
OS errors, while letting all other exceptions propagate.
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
@ -945,10 +950,10 @@ while letting all other exceptions propagate.
|
|||
Caught Exception Objects
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It is important to point out that the ``ExceptionGroup`` bound to ``e`` is an
|
||||
ephemeral object. Raising it via ``raise`` or ``raise e`` will not cause changes
|
||||
to the overall shape of the ``ExceptionGroup``. Any modifications to it will
|
||||
likely be lost:
|
||||
It is important to point out that the exception group bound to ``e`` in an
|
||||
``except*`` clause is an ephemeral object. Raising it via ``raise`` or
|
||||
``raise e`` will not cause changes to the overall shape of the original
|
||||
exception group. Any modifications to ``e`` will likely be lost:
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
@ -981,8 +986,9 @@ It is not possible to use both traditional ``except`` blocks and the new
|
|||
pass # combining ``except`` and ``except*``
|
||||
# is prohibited
|
||||
|
||||
It is possible to catch the ``ExceptionGroup`` type with ``except``, but not
|
||||
with ``except*`` because the latter is ambiguous:
|
||||
It is possible to catch the ``ExceptionGroup`` and ``BaseExceptionGroup``
|
||||
types with ``except``, but not with ``except*`` because the latter is
|
||||
ambiguous:
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
@ -1027,13 +1033,13 @@ Backwards Compatibility
|
|||
Backwards compatibility was a requirement of our design, and the changes we
|
||||
propose in this PEP will not break any existing code:
|
||||
|
||||
* The addition of a new builtin exception type ``ExceptionGroup`` does not impact
|
||||
existing programs. The way that existing exceptions are handled and displayed
|
||||
does not change in any way.
|
||||
* The addition of the new builtin exception types ``ExceptionGroup`` and
|
||||
``BaseExceptionGroup`` does not impact existing programs. The way that
|
||||
existing exceptions are handled and displayed does not change in any way.
|
||||
|
||||
* The behaviour of ``except`` is unchanged so existing code will continue to work.
|
||||
Programs will only be impacted by the changes proposed in this PEP once they
|
||||
begin to use ``ExceptionGroups`` and ``except*``.
|
||||
begin to use exception groups and ``except*``.
|
||||
|
||||
* An important concern was that ``except Exception:`` will continue to catch
|
||||
almost all exceptions, and by making ``ExceptionGroup`` extend ``Exception``
|
||||
|
@ -1045,20 +1051,20 @@ Once programs begin to use these features, there will be migration issues to
|
|||
consider:
|
||||
|
||||
* An ``except T:`` clause that wraps code which is now potentially raising
|
||||
``ExceptionGroup`` may need to become ``except *T:``, and its body may
|
||||
need to be updated. This means that raising an ``ExceptionGroup`` is an
|
||||
an exception group may need to become ``except *T:``, and its body may
|
||||
need to be updated. This means that raising an exception group is an
|
||||
API-breaking change and will likely be done in new APIs rather than
|
||||
added to existing ones.
|
||||
|
||||
* Libraries that need to support older Python versions will not be able to use
|
||||
``except*`` or raise ``ExceptionGroups``.
|
||||
``except*`` or raise exception groups.
|
||||
|
||||
|
||||
How to Teach This
|
||||
=================
|
||||
|
||||
``ExceptionGroups`` and ``except*`` will be documented as part of the language
|
||||
standard. Libraries that raise ``ExceptionGroups`` such as ``asyncio`` will need
|
||||
Exception groups and ``except*`` will be documented as part of the language
|
||||
standard. Libraries that raise exception groups such as ``asyncio`` will need
|
||||
to specify this in their documentation and clarify which API calls need to be
|
||||
wrapped with ``try-except*`` rather than ``try-except``.
|
||||
|
||||
|
@ -1071,7 +1077,8 @@ the help of the reference implementation [11]_.
|
|||
|
||||
It has the builtin ``ExceptionGroup`` along with the changes to the traceback
|
||||
formatting code, in addition to the grammar, compiler and interpreter changes
|
||||
required to support ``except*``.
|
||||
required to support ``except*``. ``BaseExceptionGroup`` will be added
|
||||
soon.
|
||||
|
||||
Two opcodes were added: one implements the exception type match check via
|
||||
``ExceptionGroup.split()``, and the other is used at the end of a ``try-except``
|
||||
|
@ -1088,19 +1095,19 @@ metadata as the original, while the raised ones do not.
|
|||
Rejected Ideas
|
||||
==============
|
||||
|
||||
Make ExceptionGroup Iterable
|
||||
----------------------------
|
||||
Make Exception Groups Iterable
|
||||
------------------------------
|
||||
|
||||
We considered making ``ExceptionGroups`` iterable, so that ``list(eg)`` would
|
||||
We considered making exception groups iterable, so that ``list(eg)`` would
|
||||
produce a flattened list of the leaf exceptions contained in the group.
|
||||
We decided that this would not be a sound API, because the metadata
|
||||
(cause, context and traceback) of the individual exceptions in a group is
|
||||
incomplete and this could create problems.
|
||||
|
||||
Furthermore, as we explained in the `Handling ExceptionGroups`_ section, we
|
||||
Furthermore, as we explained in the `Handling Exception Groups`_ section, we
|
||||
find it unlikely that iteration over leaf exceptions will have many use cases.
|
||||
We did, however, provide there the code for a traversal algorithm that
|
||||
correctly constructs each leaf exception's metadata. If it does turn out to
|
||||
correctly constructs each leaf exceptions' metadata. If it does turn out to
|
||||
be useful in practice, we can add that utility to the standard library.
|
||||
|
||||
Traceback Representation
|
||||
|
@ -1115,11 +1122,11 @@ group. Furthermore, a useful display of the traceback includes information
|
|||
about the nested exceptions. For these reasons we decided that it is best to
|
||||
leave the traceback mechanism as it is and modify the traceback display code.
|
||||
|
||||
Extend ``except`` to handle ``ExceptionGroups``
|
||||
-----------------------------------------------
|
||||
Extend ``except`` to Handle Exception Groups
|
||||
---------------------------------------------
|
||||
|
||||
We considered extending the semantics of ``except`` to handle
|
||||
``ExceptionGroups``, instead of introducing ``except*``. There were two
|
||||
exception groups, instead of introducing ``except*``. There were two
|
||||
backwards compatibility concerns with this. The first is the type of the
|
||||
caught exception. Consider this example:
|
||||
|
||||
|
@ -1131,7 +1138,7 @@ caught exception. Consider this example:
|
|||
if err.errno != ENOENT:
|
||||
raise
|
||||
|
||||
If the value assigned to err is an ``ExceptionGroup`` containing all of
|
||||
If the value assigned to err is an exception group containing all of
|
||||
the ``OSErrors`` that were raised, then the attribute access ``err.errno``
|
||||
no longer works. So we would need to execute the body of the ``except``
|
||||
clause multiple times, once for each exception in the group. However, this
|
||||
|
@ -1140,29 +1147,30 @@ clauses with the knowledge that they are only executed once. If there is
|
|||
a non-idempotent operation there, such as releasing a resource, the
|
||||
repetition could be harmful.
|
||||
|
||||
A new ``except`` alternative
|
||||
A New ``except`` Alternative
|
||||
----------------------------
|
||||
|
||||
We considered introducing a new keyword (such as ``catch``) which can be used
|
||||
to handle both naked exceptions and ``ExceptionGroups``. Its semantics would
|
||||
be the same as those of ``except*`` when catching an ``ExceptionGroup``, but
|
||||
it would not wrap a naked exception to create an ``ExceptionGroup``. This
|
||||
to handle both naked exceptions and exception groups. Its semantics would
|
||||
be the same as those of ``except*`` when catching an exception group, but
|
||||
it would not wrap a naked exception to create an exception group. This
|
||||
would have been part of a long term plan to replace ``except`` by ``catch``,
|
||||
but we decided that deprecating ``except`` in favour of an enhanced keyword
|
||||
would be too confusing for users at this time, so it is more appropriate
|
||||
to introduce the ``except*`` syntax for ``ExceptionGroups`` while ``except``
|
||||
to introduce the ``except*`` syntax for exception groups while ``except``
|
||||
continues to be used for simple exceptions.
|
||||
|
||||
Applying an ``except*`` clause on one exception at a time
|
||||
Applying an ``except*`` Clause on One Exception at a Time
|
||||
---------------------------------------------------------
|
||||
|
||||
We explained above why it is unsafe to execute an ``except`` clause in existing
|
||||
code more than once. We considered doing this in the new ``except*`` clauses,
|
||||
We explained above that it is unsafe to execute an ``except`` clause in
|
||||
existing code more than once, because the code may not be idempotent.
|
||||
We considered doing this in the new ``except*`` clauses,
|
||||
where the backwards compatibility considerations do not exist.
|
||||
The idea is to always execute an ``except*`` clause on a single exception,
|
||||
possibly executing the same clause multiple times when it matches multiple
|
||||
exceptions. We decided instead to execute each ``except*`` clause at most once,
|
||||
giving it an ``ExceptionGroup`` that contains all matching exceptions. The
|
||||
exceptions. We decided instead to execute each ``except*`` clause at most
|
||||
once, giving it an exception group that contains all matching exceptions. The
|
||||
reason for this decision was the observation that when a program needs to know
|
||||
the particular context of an exception it is handling, the exception is
|
||||
handled before it is grouped and raised together with other exceptions.
|
||||
|
@ -1197,30 +1205,30 @@ or cleanup is required. This decision is likely to be the same whether the group
|
|||
contains a single or multiple instances of something like a ``KeyboardInterrupt``
|
||||
or ``asyncio.CancelledError``. Therefore, it is more convenient to handle all
|
||||
exceptions matching an ``except*`` at once. If it does turn out to be necessary,
|
||||
the handler can inpect the ``ExceptionGroup`` and process the individual
|
||||
the handler can inpect the exception group and process the individual
|
||||
exceptions in it.
|
||||
|
||||
Not matching naked exceptions in ``except*``
|
||||
Not Matching Naked Exceptions in ``except*``
|
||||
--------------------------------------------
|
||||
|
||||
We considered the option of making ``except *T`` match only ``ExceptionGroups``
|
||||
We considered the option of making ``except *T`` match only exception groups
|
||||
that contain ``Ts``, but not naked ``Ts``. To see why we thought this would
|
||||
not be a desirable feature, return to the distinction in the previous paragraph
|
||||
between operation errors and control flow exceptions. If we don't know whether
|
||||
we should expect naked exceptions or ``ExceptionGroups`` from the body of a
|
||||
we should expect naked exceptions or exception groups from the body of a
|
||||
``try`` block, then we're not in the position of handling operation errors.
|
||||
Rather, we are likely calling a fairly generic function and will be handling
|
||||
errors to make control flow decisions. We are likely to do the same thing
|
||||
whether we catch a naked exception of type ``T`` or an ``ExceptionGroup``
|
||||
whether we catch a naked exception of type ``T`` or an exception group
|
||||
with one or more ``Ts``. Therefore, the burden of having to explicitly handle
|
||||
both is not likely to have semantic benefit.
|
||||
|
||||
If it does turn out to be necessary to make the distinction, it is always
|
||||
possible to nest in the ``try-except*`` clause an additional ``try-except`` clause
|
||||
which intercepts and handles a naked exception before the ``except*`` clause
|
||||
has a change to wrap it in an ``ExceptionGroup``. In this case the overhead
|
||||
of specifying both is not additional burden - we really do need to write a
|
||||
separate code block to handle each case:
|
||||
possible to nest in the ``try-except*`` clause an additional ``try-except``
|
||||
clause which intercepts and handles a naked exception before the ``except*``
|
||||
clause has a chance to wrap it in an exception group. In this case the
|
||||
overhead of specifying both is not additional burden - we really do need to
|
||||
write a separate code block to handle each case:
|
||||
|
||||
.. code-block::
|
||||
|
||||
|
@ -1230,7 +1238,7 @@ separate code block to handle each case:
|
|||
except SomeError:
|
||||
# handle the naked exception
|
||||
except *SomeError:
|
||||
# handle the ExceptionGroup
|
||||
# handle the exception group
|
||||
|
||||
|
||||
Allow mixing ``except:`` and ``except*:`` in the same ``try``
|
||||
|
@ -1239,7 +1247,7 @@ Allow mixing ``except:`` and ``except*:`` in the same ``try``
|
|||
This option was rejected because it adds complexity without adding useful
|
||||
semantics. Presumably the intention would be that an ``except T:`` block handles
|
||||
only naked exceptions of type ``T``, while ``except *T:`` handles ``T`` in
|
||||
``ExceptionGroups``. We already discussed above why this is unlikely
|
||||
exception groups. We already discussed above why this is unlikely
|
||||
to be useful in practice, and if it is needed then the nested ``try-except``
|
||||
block can be used instead to achieve the same result.
|
||||
|
||||
|
@ -1249,7 +1257,7 @@ block can be used instead to achieve the same result.
|
|||
Since either all or none of the clauses of a ``try`` construct are ``except*``,
|
||||
we considered changing the syntax of the ``try`` instead of all the ``except*``
|
||||
clauses. We rejected this because it would be less obvious. The fact that we
|
||||
are handling ``ExceptionGroups`` of ``T`` rather than only naked ``Ts`` should be
|
||||
are handling exception groups of ``T`` rather than only naked ``Ts`` should be
|
||||
specified in the same place where we state ``T``.
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue