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

Commit 44593ee

Browse files
authored
Refined except* semantics
* edit raise in except* example * changed examples and description of algo to correctly split-merge exception groups with their metadata preserved * fix example * added comment about 'except *ExceptionGroup'
1 parent 3ebd257 commit 44593ee

File tree

1 file changed

+102
-46
lines changed

1 file changed

+102
-46
lines changed

except_star.md

Lines changed: 102 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,15 @@ The `except *SpamError` block will be run if the `try` code raised an
5555
`ExceptionGroup` with one or more instances of `SpamError`. It would also be
5656
triggered if a naked instance of `SpamError` was raised.
5757

58-
The `except *BazError as e` block would aggregate all instances of `BazError`
59-
into a list, wrap that list into an `ExceptionGroup` instance, and assign
60-
the resultant object to `e`. The type of `e` would be
61-
`ExceptionGroup[BazError]`. If there was just one naked instance of
62-
`BazError`, it would be wrapped into an `ExceptionGroup` and assigned to `e`.
63-
64-
The `except *(BarError, FooError) as e` would aggregate all instances of
65-
`BarError` or `FooError` into a list and assign that wrapped list to `e`.
58+
The `except *BazError as e` block would create an ExceptionGroup with the
59+
same nested structure and metadata (msg, cause and context) as the one
60+
raised, but containing only the instances of `BazError`. This ExceptionGroup
61+
is assigned to `e`. The type of `e` would be `ExceptionGroup[BazError]`.
62+
If there was just one naked instance of `BazError`, it would be wrapped
63+
into an `ExceptionGroup` and assigned to `e`.
64+
65+
The `except *(BarError, FooError) as e` would split out all instances of
66+
`BarError` or `FooError` into such an ExceptionGroup and assign it to `e`.
6667
The type of `e` would be `ExceptionGroup[Union[BarError, FooError]]`.
6768

6869
Even though every `except*` clause can be executed only once, any number of
@@ -86,6 +87,23 @@ except *CancelledError: # <- SyntaxError:
8687
pass # combining `except` and `except*` is prohibited
8788
```
8889

90+
It is possible to catch the `ExceptionGroup` type with a plain except, but not
91+
with an `except*` because the latter is ambiguous:
92+
93+
```python
94+
try:
95+
...
96+
except ExceptionGroup: # <- This works
97+
pass
98+
99+
100+
try:
101+
...
102+
except *ExceptionGroup: # <- Runtime error
103+
pass
104+
```
105+
106+
89107
Exceptions are matched using a subclass check. For example:
90108

91109
```python
@@ -113,7 +131,7 @@ Example:
113131
```python
114132
try:
115133
raise ExceptionGroup(
116-
ValueError('a'), TypeError('b'), TypeError('c'), KeyError('e')
134+
"msg", ValueError('a'), TypeError('b'), TypeError('c'), KeyError('e')
117135
)
118136
except *ValueError as e:
119137
print(f'got some ValueErrors: {e}')
@@ -125,14 +143,15 @@ except *TypeError as e:
125143
The above code would print:
126144

127145
```
128-
got some ValueErrors: ExceptionGroup(ValueError('a'))
129-
got some TypeErrors: ExceptionGroup(TypeError('b'), TypeError('c'))
146+
got some ValueErrors: ExceptionGroup("msg", ValueError('a'))
147+
got some TypeErrors: ExceptionGroup("msg", TypeError('b'), TypeError('c'))
130148
```
131149

132150
and then terminate with an unhandled `ExceptionGroup`:
133151

134152
```
135153
ExceptionGroup(
154+
"msg",
136155
TypeError('b'),
137156
TypeError('c'),
138157
KeyError('e'),
@@ -143,31 +162,34 @@ Basically, before interpreting `except *` clauses, the interpreter will
143162
have an "incoming" `ExceptionGroup` object with a list of exceptions in it
144163
to handle, and then:
145164

146-
* A new empty "result" `ExceptionGroup` would be created by the interpreter.
165+
* The interpreter creates two new empty result lists for the exceptions that
166+
will be raised in the except* blocks: a "reraised" list for the naked raises
167+
and a "raised" list of the parameterised raises.
147168

148169
* Every `except *` clause, run from top to bottom, can match a subset of the
149170
exceptions out of the group forming a "working set" of errors for the current
150-
clause. If the except block raises an exception, that exception is added
151-
to the "result" `ExceptionGroup` (with its "working set" of errors
152-
linked to that exception via the `__context__` attribute.)
171+
clause. These exceptions are removed from the "incoming" group. If the except
172+
block raises an exception, that exception is added to the appropriate result
173+
list ("raised" or "reraised"), and in the case of "raise" it gets its
174+
"working set" of errors linked to it via the `__context__` attribute.
153175

154176
* After there are no more `except*` clauses to evaluate, there are the
155177
following possibilities:
156178

157-
* Both "incoming" and "result" `ExceptionGroup`s are empty. This means
158-
that all exceptions were processed and silenced.
179+
* Both the "incoming" `ExceptionGroup` and the two result lists are empty. This
180+
means that all exceptions were processed and silenced.
181+
182+
* The "incoming" `ExceptionGroup` is non-empty but the result lists are:
183+
not all exceptions were processed. The interpreter raises the "incoming" group.
159184

160-
* Both "incoming" and "result" `ExceptionGroup`s are not empty.
161-
This means that not all of the exceptions were matched, and some were
162-
matched but either triggered new errors, or were re-raised. The interpreter
163-
would merge both groups into one group and raise it.
185+
* At least one of the result lists is non-empty: there are exceptions raised
186+
from the except* clauses. The interpreter constructs a new `ExceptionGroup` with
187+
an empty message and an exception list that contains all exceptions in "raised"
188+
in addition to a single ExceptionGroup which holds the exceptions in "reraised"
189+
and "incoming", in the same nested structure and with the same metadata as in
190+
the original incoming exception.
164191

165-
* The "incoming" `ExceptionGroup` is non-empty: not all exceptions were
166-
processed. The interpreter would raise the "incoming" group.
167192

168-
* The "result" `ExceptionGroup` is non-empty: all exceptions were processed,
169-
but some were re-raised or caused new errors. The interpreter would
170-
raise the "result" group.
171193

172194
The order of `except*` clauses is significant just like with the regular
173195
`try..except`, e.g.:
@@ -189,7 +211,7 @@ except *BlockingIOError:
189211

190212
### Raising ExceptionGroups manually
191213

192-
Exception groups can be raised manually:
214+
Exception groups can be created and raised as follows:
193215

194216
```python
195217
try:
@@ -199,44 +221,52 @@ except *OSerror as errors:
199221
for e in errors:
200222
if e.errno != errno.EPIPE:
201223
new_errors.append(e)
202-
raise ExceptionGroup(*new_errors)
224+
raise ExceptionGroup(errors.msg, *new_errors)
203225
```
204226

205227
The above code ignores all `EPIPE` OS errors, while letting all other
206228
exceptions propagate.
207229

208-
Raising an `ExceptionGroup` introduces nesting:
230+
Raising exceptions while handling an `ExceptionGroup` introduces nesting because
231+
the traceback and chaining information needs to be maintained:
209232

210233
```python
211234
try:
212-
raise ExceptionGroup(ValueError('a'), TypeError('b'))
235+
raise ExceptionGroup("one", ValueError('a'), TypeError('b'))
213236
except *ValueError:
214-
raise ExceptionGroup(KeyError('x'), KeyError('y'))
237+
raise ExceptionGroup("two", KeyError('x'), KeyError('y'))
215238

216239
# would result in:
217240
#
218241
# ExceptionGroup(
219-
# ExceptionGroup(
242+
# "",
243+
# ExceptionGroup( <-- context = ExceptionGroup(ValueError('a'))
244+
# "two",
220245
# KeyError('x'),
221246
# KeyError('y'),
222247
# ),
223-
# TypeError('b'),
248+
# ExceptionGroup( <-- context, cause, tb same as the original "one"
249+
# "one",
250+
# TypeError('b'),
251+
# )
224252
# )
225253
```
226254

227-
Although a regular `raise Exception` would not wrap `Exception` in a group:
255+
A regular `raise Exception` would not wrap `Exception` in its own group, but a new group would still be
256+
created to merged it with the ExceptionGroup of unhandled exceptions:
228257

229258
```python
230259
try:
231-
raise ExceptionGroup(ValueError('a'), TypeError('b'))
260+
raise ExceptionGroup("eg", ValueError('a'), TypeError('b'))
232261
except *ValueError:
233262
raise KeyError('x')
234263

235264
# would result in:
236265
#
237266
# ExceptionGroup(
267+
# "",
238268
# KeyError('x'),
239-
# TypeError('b')
269+
# ExceptionGroup("eg", TypeError('b'))
240270
# )
241271
```
242272

@@ -248,21 +278,26 @@ referenced from the just occurred exception via its `__context__` attribute:
248278

249279
```python
250280
try:
251-
raise ExceptionGroup(ValueError('a'), ValueError('b'), TypeError('z'))
281+
raise ExceptionGroup("eg", ValueError('a'), ValueError('b'), TypeError('z'))
252282
except *ValueError:
253283
1 / 0
254284

255285
# would result in:
256286
#
257287
# ExceptionGroup(
258-
# TypeError('z'),
288+
# "",
289+
# ExceptionGroup(
290+
# "eg",
291+
# TypeError('z'),
292+
# ),
259293
# ZeroDivisionError()
260294
# )
261295
#
262296
# where the `ZeroDivisionError()` instance would have
263297
# its __context__ attribute set to
264298
#
265299
# ExceptionGroup(
300+
# "eg",
266301
# ValueError('a'),
267302
# ValueError('b')
268303
# )
@@ -272,21 +307,25 @@ It's also possible to explicitly chain exceptions:
272307

273308
```python
274309
try:
275-
raise ExceptionGroup(ValueError('a'), ValueError('b'), TypeError('z'))
310+
raise ExceptionGroup("eg", ValueError('a'), ValueError('b'), TypeError('z'))
276311
except *ValueError as errors:
277312
raise RuntimeError('unexpected values') from errors
278313

279314
# would result in:
280315
#
281316
# ExceptionGroup(
282-
# TypeError('z'),
317+
# ExceptionGroup(
318+
# "eg",
319+
# TypeError('z'),
320+
# ),
283321
# RuntimeError('unexpected values')
284322
# )
285323
#
286324
# where the `RuntimeError()` instance would have
287325
# its __cause__ attribute set to
288326
#
289327
# ExceptionGroup(
328+
# "eg",
290329
# ValueError('a'),
291330
# ValueError('b')
292331
# )
@@ -300,21 +339,23 @@ recursively. E.g.:
300339
```python
301340
try:
302341
raise ExceptionGroup(
342+
"eg",
303343
ValueError('a'),
304344
TypeError('b'),
305345
ExceptionGroup(
346+
"nested",
306347
TypeError('c'),
307348
KeyError('d')
308349
)
309350
)
310351
except *TypeError as e:
311-
print(f'got some TypeErrors: {list(e)}')
352+
print(f'e = {e}')
312353
except *Exception:
313354
pass
314355

315356
# would print:
316357
#
317-
# got some TypeErrors: [TypeError('b'), TypeError('c')]
358+
# e = ExceptionGroup("eg", TypeError('b'), ExceptionGroup("nested", TypeError('c'))
318359
```
319360

320361
Iteration over an `ExceptionGroup` that has nested `ExceptionGroup` objects
@@ -324,9 +365,11 @@ in it effectively flattens the entire tree. E.g.
324365
print(
325366
list(
326367
ExceptionGroup(
368+
"eg",
327369
ValueError('a'),
328370
TypeError('b'),
329371
ExceptionGroup(
372+
"nested",
330373
TypeError('c'),
331374
KeyError('d')
332375
)
@@ -349,9 +392,11 @@ likely get lost:
349392
```python
350393
try:
351394
raise ExceptionGroup(
395+
"top",
352396
ValueError('a'),
353397
TypeError('b'),
354398
ExceptionGroup(
399+
"nested",
355400
TypeError('c'),
356401
KeyError('d')
357402
)
@@ -369,26 +414,33 @@ If the user wants to "flatten" the tree, they can explicitly create a new
369414
```python
370415
try:
371416
raise ExceptionGroup(
417+
"one",
372418
ValueError('a'),
373419
TypeError('b'),
374420
ExceptionGroup(
421+
"two",
375422
TypeError('c'),
376423
KeyError('d')
377424
)
378425
)
379426
except *TypeError as e:
380-
raise ExceptionGroup(*e)
427+
raise ExceptionGroup("three", *e)
381428

382429
# would terminate with:
383430
#
384431
# ExceptionGroup(
385-
# ValueError('a'),
432+
# "three",
386433
# ExceptionGroup(
387434
# TypeError('b'),
388435
# TypeError('c'),
389436
# ),
390437
# ExceptionGroup(
391-
# KeyError('d')
438+
# "one",
439+
# ValueError('a'),
440+
# ExceptionGroup(
441+
# "two",
442+
# KeyError('d')
443+
# )
392444
# )
393445
# )
394446
```
@@ -430,9 +482,11 @@ group:
430482
```python
431483
try:
432484
raise ExceptionGroup(
485+
"one",
433486
ValueError('a'),
434487
TypeError('b'),
435488
ExceptionGroup(
489+
"two",
436490
TypeError('c'),
437491
KeyError('d')
438492
)
@@ -443,9 +497,11 @@ except *TypeError as e:
443497
# would both terminate with:
444498
#
445499
# ExceptionGroup(
500+
# "one",
446501
# ValueError('a'),
447502
# TypeError('b'),
448503
# ExceptionGroup(
504+
# "two",
449505
# TypeError('c'),
450506
# KeyError('d')
451507
# )
@@ -462,7 +518,7 @@ Consider if they were allowed:
462518
```python
463519
def foo():
464520
try:
465-
raise ExceptionGroup(A(), B())
521+
raise ExceptionGroup("msg", A(), B())
466522
except *A:
467523
return 1
468524
except *B:

0 commit comments

Comments
 (0)