Skip to content
This repository was archived by the owner on Apr 10, 2022. It is now read-only.

Commit a9068e4

Browse files
authored
Typos. Reference Implementation description. Added a rejected idea. (#21)
* Typos. Reference Implementation description. Added a rejected idea. * typo
1 parent ef91921 commit a9068e4

File tree

1 file changed

+56
-33
lines changed

1 file changed

+56
-33
lines changed

except_star.md

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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

7777
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.
8485

8586
Changes to the language are required in order to extend support for
8687
`ExceptionGroup`s in the style of existing exception handling mechanisms. At
8788
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
8990
other types in the same `ExceptionGroup` need to be automatically reraised,
9091
otherwise it is too easy for user code to inadvertently swallow exceptions
9192
that 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`
115116
and `errors`. A nested exception can also be an `ExceptionGroup` so the class
116117
represents a tree of exceptions, where the leaves are plain exceptions and
117118
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.
119120

120121
The `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
163164
fully contained in the result. When it is necessary to partition an
164165
`ExceptionGroup` because the condition holds for some, but not all of its
165166
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
167168
with 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
274275
simplify working with exception groups. The `*` symbol indicates that multiple
275276
exceptions 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()])`.
300301
The `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)])`
308308
and `rest = ExceptionGroup('msg', [BazError()])`. The body of this `except*`
309309
block is executed, with the value of `e` and `sys.exc_info()` set to `match`.
310310
Then, `unhandled` is set to `rest`.
@@ -332,7 +332,7 @@ InterruptedError
332332
BlockingIOError
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

399399
If the exception raised inside the `try` body is not of type `ExceptionGroup`,
400400
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:
403403

404404
```python
405405
>>> try:
@@ -454,7 +454,7 @@ ZeroDivisionError: division by zero |
454454
```
455455

456456
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*`
458458
clauses, as well as unhandled exceptions that need to propagate.
459459
The interpreter needs to combine all those exceptions into a result, and
460460
raise that.
@@ -466,9 +466,9 @@ metadata - the traceback contains the line from which it was raised, its
466466
cause is whatever it may have been explicitly chained to, and its context is the
467467
value 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
470470
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
472472
inner `try-except*` block raises an `ExceptionGroup` that contains all
473473
`ValueError`s and `TypeError`s merged back into the same shape they had in
474474
the original `ExceptionGroup`:
@@ -747,7 +747,7 @@ except *OSerror as errors:
747747
It is important to point out that the `ExceptionGroup` bound to `e` is an
748748
ephemeral object. Raising it via `raise` or `raise e` will not cause changes
749749
to 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
810810
independent, and the presence or absence of one of them should not impact
811811
handling of the others, as could happen if we allow an `except*` clause to
812812
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:
814814

815815
```python
816816
def 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
980996
to be useful in practice, and if it is needed then the nested `try-except`
981997
block 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

Comments
 (0)