@@ -31,7 +31,7 @@ unwinds. Several real world use cases are listed below.
31
31
collection of errors. Work on this PEP was initially motivated by the
32
32
difficulties in handling ` MultiError ` s, which are detailed in a design
33
33
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 ) .
35
35
That document demonstrates how difficult it is to create an effective API
36
36
for reporting and handling multiple errors without the language changes we
37
37
are proposing.
@@ -75,17 +75,18 @@ unwinds. Several real world use cases are listed below.
75
75
## Rationale
76
76
77
77
Grouping 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.
84
85
85
86
Changes to the language are required in order to extend support for
86
87
` ExceptionGroup ` s in the style of existing exception handling mechanisms. At
87
88
the 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
89
90
other types in the same ` ExceptionGroup ` need to be automatically reraised,
90
91
otherwise it is too easy for user code to inadvertently swallow exceptions
91
92
that it is not handling.
@@ -94,7 +95,7 @@ The purpose of this PEP, then, is to add the `except*` syntax for handling
94
95
` ExceptionGroups ` s in the interpreter, which in turn requires that
95
96
` ExceptionGroup ` is added as a builtin type. The semantics of handling
96
97
` 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
98
99
` except ` keyword but rather to add the new ` except* ` syntax.
99
100
100
101
@@ -115,7 +116,7 @@ The `ExceptionGroup` class exposes these parameters in the fields `message`
115
116
and ` errors ` . A nested exception can also be an ` ExceptionGroup ` so the class
116
117
represents a tree of exceptions, where the leaves are plain exceptions and
117
118
each 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 .
119
120
120
121
The ` ExceptionGroup.subgroup(condition) ` method gives us a way to obtain an
121
122
` 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
163
164
fully contained in the result. When it is necessary to partition an
164
165
` ExceptionGroup ` because the condition holds for some, but not all of its
165
166
contained 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
167
168
with the original ` eg ` .
168
169
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:
171
172
172
173
``` Python
173
174
>> > type_errors, other_errors = eg.split(lambda e : isinstance (e, TypeError ))
@@ -270,7 +271,7 @@ ExceptionGroup: two
270
271
271
272
### except*
272
273
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
274
275
simplify working with exception groups. The ` * ` symbol indicates that multiple
275
276
exceptions can be handled by each ` except* ` clause:
276
277
@@ -299,12 +300,11 @@ For example, suppose that the body of the `try` block above raises
299
300
` eg = ExceptionGroup('msg', [FooError(1), FooError(2), BazError()]) ` .
300
301
The ` except* ` clauses are evaluated in order by calling ` split ` on the
301
302
` 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)]) `
308
308
and ` rest = ExceptionGroup('msg', [BazError()]) ` . The body of this ` except* `
309
309
block is executed, with the value of ` e ` and ` sys.exc_info() ` set to ` match ` .
310
310
Then, ` unhandled ` is set to ` rest ` .
@@ -332,7 +332,7 @@ InterruptedError
332
332
BlockingIOError
333
333
```
334
334
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
336
336
` try..except ` :
337
337
338
338
``` python
@@ -398,8 +398,8 @@ propagated: ExceptionGroup('msg', [KeyError('e')])
398
398
399
399
If the exception raised inside the ` try ` body is not of type ` ExceptionGroup ` ,
400
400
we 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:
403
403
404
404
``` python
405
405
>> > try :
@@ -454,7 +454,7 @@ ZeroDivisionError: division by zero |
454
454
```
455
455
456
456
This 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* `
458
458
clauses, as well as unhandled exceptions that need to propagate.
459
459
The interpreter needs to combine all those exceptions into a result, and
460
460
raise that.
@@ -466,9 +466,9 @@ metadata - the traceback contains the line from which it was raised, its
466
466
cause is whatever it may have been explicitly chained to, and its context is the
467
467
value of ` sys.exc_info() ` in the ` except* ` clause of the raise.
468
468
469
- In the aggregated ` ExceptionGroup ` , the reraised and unhandled exceptions have
469
+ In the aggregated ` ExceptionGroup ` , the reraised and unhandled exceptions have
470
470
the 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
472
472
inner ` try-except* ` block raises an ` ExceptionGroup ` that contains all
473
473
` ValueError ` s and ` TypeError ` s merged back into the same shape they had in
474
474
the original ` ExceptionGroup ` :
@@ -747,7 +747,7 @@ except *OSerror as errors:
747
747
It is important to point out that the ` ExceptionGroup ` bound to ` e ` is an
748
748
ephemeral object. Raising it via ` raise ` or ` raise e ` will not cause changes
749
749
to the overall shape of the ` ExceptionGroup ` . Any modifications to it will
750
- likely get lost:
750
+ likely be lost:
751
751
752
752
``` python
753
753
>> > eg = ExceptionGroup(" eg" , [TypeError (12 )])
@@ -765,8 +765,8 @@ likely get lost:
765
765
766
766
### Forbidden Combinations
767
767
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
770
770
` SyntaxErorr ` :
771
771
772
772
``` python
@@ -810,7 +810,7 @@ This is because the exceptions in an `ExceptionGroup` are assumed to be
810
810
independent, and the presence or absence of one of them should not impact
811
811
handling of the others, as could happen if we allow an ` except* ` clause to
812
812
change 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:
814
814
815
815
``` python
816
816
def foo ():
@@ -819,7 +819,7 @@ def foo():
819
819
except * A:
820
820
return 1 # <- SyntaxError
821
821
except * B as e:
822
- raise TypeError (" Can't have B without A!" ) from e
822
+ raise TypeError (" Can't have B without A!" )
823
823
```
824
824
825
825
## Backwards Compatibility
@@ -857,8 +857,24 @@ to be updated.
857
857
858
858
## Reference Implementation
859
859
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.
862
878
863
879
## Rejected Ideas
864
880
@@ -980,6 +996,13 @@ only naked exceptions of type `T`, while `except *T:` handles `T` in
980
996
to be useful in practice, and if it is needed then the nested ` try-except `
981
997
block can be used instead to achieve the same result.
982
998
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 ` .
983
1006
984
1007
## See Also
985
1008
0 commit comments