@@ -31,7 +31,7 @@ unwinds. Several real world use cases are listed below.
3131 collection of errors. Work on this PEP was initially motivated by the
3232 difficulties in handling ` MultiError ` s, which are detailed in a design
3333 document for an
34- [ improved version, ` MultiError2 ` ] ( [ https://github.com/python-trio/trio/issues/611) .
34+ [ improved version, ` MultiError2 ` ] ( https://github.com/python-trio/trio/issues/611 ) .
3535 That document demonstrates how difficult it is to create an effective API
3636 for reporting and handling multiple errors without the language changes we
3737 are proposing.
@@ -75,17 +75,18 @@ unwinds. Several real world use cases are listed below.
7575## Rationale
7676
7777Grouping several exceptions together can be done without changes to the
78- language, simply by creating a container exception type. Trio is an example of
79- a library that has made use of this technique in its ` MultiError ` type
80- [ reference to Trio MultiError] . However, such approaches require calling code
81- to catch the container exception type, and then inspect it to determine the
82- types of errors that had occurred, extract the ones it wants to handle and
83- reraise the rest.
78+ language, simply by creating a container exception type.
79+ [ Trio] ( https://trio.readthedocs.io/en/stable/ ) is an example of a library that
80+ has made use of this technique in its
81+ [ ` MultiError ` type] ( https://trio.readthedocs.io/en/stable/reference-core.html#trio.MultiError ) .
82+ However, such approaches require calling code to catch the container exception
83+ type, and then inspect it to determine the types of errors that had occurred,
84+ extract the ones it wants to handle and reraise the rest.
8485
8586Changes to the language are required in order to extend support for
8687` ExceptionGroup ` s in the style of existing exception handling mechanisms. At
8788the very least we would like to be able to catch an ` ExceptionGroup ` only if
88- it contains an exception type that we that chose to handle. Exceptions of
89+ it contains an exception type that we choose to handle. Exceptions of
8990other types in the same ` ExceptionGroup ` need to be automatically reraised,
9091otherwise it is too easy for user code to inadvertently swallow exceptions
9192that it is not handling.
@@ -94,7 +95,7 @@ The purpose of this PEP, then, is to add the `except*` syntax for handling
9495` ExceptionGroups ` s in the interpreter, which in turn requires that
9596` ExceptionGroup ` is added as a builtin type. The semantics of handling
9697` ExceptionGroup ` s are not backwards compatible with the current exception
97- handling semantics, so we are not proposing to modify the behaviour of the
98+ handling semantics, so we are not proposing to modify the behavior of the
9899` except ` keyword but rather to add the new ` except* ` syntax.
99100
100101
@@ -115,7 +116,7 @@ The `ExceptionGroup` class exposes these parameters in the fields `message`
115116and ` errors ` . A nested exception can also be an ` ExceptionGroup ` so the class
116117represents a tree of exceptions, where the leaves are plain exceptions and
117118each internal node represent a time at which the program grouped some
118- unrelated exceptions into a new ` ExceptionGroup ` .
119+ unrelated exceptions into a new ` ExceptionGroup ` and raised them together .
119120
120121The ` ExceptionGroup.subgroup(condition) ` method gives us a way to obtain an
121122` ExceptionGroup ` that has the same metadata (cause, context, traceback) as
@@ -163,11 +164,11 @@ new copy. Leaf exceptions are not copied, nor are `ExceptionGroup`s which are
163164fully contained in the result. When it is necessary to partition an
164165` ExceptionGroup ` because the condition holds for some, but not all of its
165166contained exceptions, a new ` ExceptionGroup ` is created but the ` __cause__ ` ,
166- ` __context__ ` and ` __traceback__ ` field are copied by reference, so are shared
167+ ` __context__ ` and ` __traceback__ ` fields are copied by reference, so are shared
167168with the original ` eg ` .
168169
169- If both the subgroup and its complement are needed, the ` ExceptionGroup.split `
170- method can be used:
170+ If both the subgroup and its complement are needed, the
171+ ` ExceptionGroup.split(condition) ` method can be used:
171172
172173``` Python
173174>> > type_errors, other_errors = eg.split(lambda e : isinstance (e, TypeError ))
@@ -270,7 +271,7 @@ ExceptionGroup: two
270271
271272### except*
272273
273- We're proposing to introduce a new variant of the ` try..except ` syntax to
274+ We are proposing to introduce a new variant of the ` try..except ` syntax to
274275simplify working with exception groups. The ` * ` symbol indicates that multiple
275276exceptions can be handled by each ` except* ` clause:
276277
@@ -299,12 +300,11 @@ For example, suppose that the body of the `try` block above raises
299300` eg = ExceptionGroup('msg', [FooError(1), FooError(2), BazError()]) ` .
300301The ` except* ` clauses are evaluated in order by calling ` split ` on the
301302` unhandled ` ` ExceptionGroup ` , which is initially equal to ` eg ` and then shrinks
302- as exceptions are matched and extracted from it.
303-
304- In our example, ` unhandled.split(SpamError) ` returns ` (None, unhandled) ` so the
305- first ` except* ` block is not executed and ` unhandled ` is unchanged. For the
306- second block, ` match, rest = unhandled.split(FooError) ` returns a non-trivial
307- split with ` match = ExceptionGroup('msg', [FooError(1), FooError(2)]) `
303+ as exceptions are matched and extracted from it. In the first ` except* ` clause,
304+ ` unhandled.split(SpamError) ` returns ` (None, unhandled) ` so the body of this
305+ block is not executed and ` unhandled ` is unchanged. For the second block,
306+ ` unhandled.split(FooError) ` returns a non-trivial split ` (match, rest) ` with
307+ ` match = ExceptionGroup('msg', [FooError(1), FooError(2)]) `
308308and ` rest = ExceptionGroup('msg', [BazError()]) ` . The body of this ` except* `
309309block is executed, with the value of ` e ` and ` sys.exc_info() ` set to ` match ` .
310310Then, ` unhandled ` is set to ` rest ` .
@@ -332,7 +332,7 @@ InterruptedError
332332BlockingIOError
333333```
334334
335- The order of ` except* ` clauses is significant just like with the regular
335+ The order of ` except* ` clauses is significant just like with the traditional
336336` try..except ` :
337337
338338``` python
@@ -398,8 +398,8 @@ propagated: ExceptionGroup('msg', [KeyError('e')])
398398
399399If the exception raised inside the ` try ` body is not of type ` ExceptionGroup ` ,
400400we call it a ` naked ` exception. If its type matches one of the ` except* `
401- clauses, it is wrapped by an ` ExceptionGroup ` with an empty message string
402- when caught . This is to make the type of ` e ` consistent and statically known:
401+ clauses, it is caught and wrapped by an ` ExceptionGroup ` with an empty message
402+ string . This is to make the type of ` e ` consistent and statically known:
403403
404404``` python
405405>> > try :
@@ -454,7 +454,7 @@ ZeroDivisionError: division by zero |
454454```
455455
456456This holds for ` ExceptionGroup ` s as well, but the situation is now more complex
457- because there can exceptions raised and reraised from multiple ` except* `
457+ because there can be exceptions raised and reraised from multiple ` except* `
458458clauses, as well as unhandled exceptions that need to propagate.
459459The interpreter needs to combine all those exceptions into a result, and
460460raise that.
@@ -466,9 +466,9 @@ metadata - the traceback contains the line from which it was raised, its
466466cause is whatever it may have been explicitly chained to, and its context is the
467467value of ` sys.exc_info() ` in the ` except* ` clause of the raise.
468468
469- In the aggregated ` ExceptionGroup ` , the reraised and unhandled exceptions have
469+ In the aggregated ` ExceptionGroup ` , the reraised and unhandled exceptions have
470470the same relative structure as in the original exception, as if they were split
471- off together in one ` subgroup ` call. For example, in the snippet below the
471+ off together in one ` subgroup ` call. For example, in the snippet below the
472472inner ` try-except* ` block raises an ` ExceptionGroup ` that contains all
473473` ValueError ` s and ` TypeError ` s merged back into the same shape they had in
474474the original ` ExceptionGroup ` :
@@ -747,7 +747,7 @@ except *OSerror as errors:
747747It is important to point out that the ` ExceptionGroup ` bound to ` e ` is an
748748ephemeral object. Raising it via ` raise ` or ` raise e ` will not cause changes
749749to the overall shape of the ` ExceptionGroup ` . Any modifications to it will
750- likely get lost:
750+ likely be lost:
751751
752752``` python
753753>> > eg = ExceptionGroup(" eg" , [TypeError (12 )])
@@ -765,8 +765,8 @@ likely get lost:
765765
766766### Forbidden Combinations
767767
768- * It is not possible to use both regular ` except ` blocks and the new ` except* `
769- clauses in the same ` try ` statement.The following example would raise a
768+ * It is not possible to use both traditional ` except ` blocks and the new
769+ ` except* ` clauses in the same ` try ` statement. The following example is a
770770` SyntaxErorr ` :
771771
772772``` python
@@ -810,7 +810,7 @@ This is because the exceptions in an `ExceptionGroup` are assumed to be
810810independent, and the presence or absence of one of them should not impact
811811handling of the others, as could happen if we allow an ` except* ` clause to
812812change the way control flows through other clauses. We believe that this is
813- error prone and there are better ways to implement a check like this:
813+ error prone and there are clearer ways to implement a check like this:
814814
815815``` python
816816def foo ():
@@ -819,7 +819,7 @@ def foo():
819819 except * A:
820820 return 1 # <- SyntaxError
821821 except * B as e:
822- raise TypeError (" Can't have B without A!" ) from e
822+ raise TypeError (" Can't have B without A!" )
823823```
824824
825825## Backwards Compatibility
@@ -857,8 +857,24 @@ to be updated.
857857
858858## Reference Implementation
859859
860- [ An experimental implementation] ( https://github.com/iritkatriel/cpython/tree/exceptionGroup-stage5 ) .
861-
860+ We developed these concepts (and the examples for this PEP) with
861+ [ an experimental implementation] ( https://github.com/iritkatriel/cpython/tree/exceptionGroup-stage5 ) .
862+
863+ It has the builtin ` ExceptionGroup ` along with the changes to the traceback
864+ formatting code, in addition to the grammar and interpreter changes required
865+ to support ` except* ` .
866+
867+ Two opcodes were added: one implements the exception type match check via
868+ ` ExceptionGroup.split() ` , and the other is used at the end of a ` try-except `
869+ construct to merge all unhandled, raised and reraised exceptions (if any).
870+ The raised/reraised exceptions are collected in a list on the runtime stack.
871+ For this purpose, the body of each ` except* ` clause is wrapped in a traditional
872+ ` try-except ` which captures any exceptions raised. Both raised and reraised
873+ exceptions are collected in one list. When the time comes to merge them into
874+ a result, the raised and reraised exceptions are distinguished by comparing
875+ their metadata fields (context, cause, traceback) with those of the originally
876+ raised exception. As mentioned above, the reraised exceptions have the same
877+ metadata as the original, while raised ones do not.
862878
863879## Rejected Ideas
864880
@@ -980,6 +996,13 @@ only naked exceptions of type `T`, while `except *T:` handles `T` in
980996to be useful in practice, and if it is needed then the nested ` try-except `
981997block can be used instead to achieve the same result.
982998
999+ ### ` try* ` instead of ` except* `
1000+
1001+ Since either all or none of the clauses of a ` try ` construct are ` except* ` ,
1002+ we considered changing the syntax of the ` try ` instead of all the ` except* `
1003+ clauses. We rejected this because it would be less obvious. The fact that we
1004+ are handling ` ExceptionGroup ` s of ` T ` rather than only naked ` T ` s should be
1005+ in the same place where we state ` T ` .
9831006
9841007## See Also
9851008
0 commit comments