Disclaimer
- I'm going to be using the
ExceptionGroup name in this issue, even though there are other alternatives, e.g. AggregateException. Naming of the "exception group" object is outside of the scope of this issue.
- This issue is primarily focused on discussing the new syntax modification proposal for the
try..except construct, shortly called "except*".
- I use the term "naked" exception for regular Python exceptions not wrapped in an ExceptionGroup. E.g. a regular
ValueError propagating through the stack is "naked".
- I assume that
ExceptionGroup would be an iterable object. E.g. list(ExceptionGroup(ValueError('a'), TypeError('b'))) would be equal to [ValueError('a'), TypeError('b')]
- I assume that
ExceptionGroup won't be an indexable object; essentially it's similar to Python set. The motivation for this is that exceptions can occur in random order, and letting users write group[0] to access the "first" error is error prone. The actual implementation of ExceptionGroup will likely use an ordered list of errors though.
- I assume that
ExceptionGroup will be a subclass of BaseException, which means it's assignable to Exception.__context__ and can be directly handled with except ExceptionGroup.
- The behavior of good and old regular
try..except will not be modified.
Syntax
We're considering to introduce a new variant of the try..except syntax to simplify working with exception groups:
try:
...
except *SpamError:
...
except *BazError as e:
...
except *(BarError, FooError) as e:
...
The new syntax can be viewed as a variant of the tuple unpacking syntax. The * symbol indicates that zero or more exceptions can be "caught" and processed by one except * clause.
We also propose to enable "unpacking" in the raise statement:
errors = (ValueError('hello'), TypeError('world'))
raise *errors
Semantics
Overview
The except *SpamError block will be run if the try code raised an ExceptionGroup with one or more instances of SpamError. It would also be triggered if a naked instance of SpamError was raised.
The except *BazError as e block would aggregate all instances of BazError into a list, wrap that list into an ExceptionGroup instance, and assign the resultant object to e. The type of e would be ExceptionGroup[BazError]. If there was just one naked instance of BazError, it would be wrapped into a list and assigned to e.
The except *(BarError, FooError) as e would aggregate all instances of BarError or FooError into a list and assign that wrapped list to e. The type of e would be ExceptionGroup[Union[BarError, FooError]].
Even though every except* star can be called only once, any number of them can be run during handling of an ExceptionGroup. E.g. in the above example, both except *SpamError: and except *(BarError, FooError) as e: could get executed during handling of one ExceptionGroup object, or all of the except* clauses, or just one of them.
It is not allowed to use both regular except clauses and the new except* clauses in the same try block. E.g. the following example would raise a SyntaxErorr:
try:
...
except ValueError:
pass
except *CancelledError:
pass
Exceptions are mached using a subclass check. For example:
try:
low_level_os_operation()
except *OSerror as errors:
for e in errors:
print(type(e).__name__)
could output:
BlockingIOError
ConnectionRefusedError
OSError
InterruptedError
BlockingIOError
New raise* Syntax
The new raise * syntax allows to users to only process some exceptions out of the matched set, e.g.:
try:
low_level_os_operation()
except *OSerror as errors:
new_errors = []
for e in errors:
if e.errno != errno.EPIPE:
new_errors.append(e)
raise *new_errors
The above code ignores all EPIPE OS errors, while letting all others propagate.
raise * syntax is special: it effectively extends the exception group with a list of errors without creating a new ExceptionGroup instance:
try:
raise *(ValueError('a'), TypeError('b'))
except *ValueError:
raise *(KeyError('x'), KeyError('y'))
# would result in:
# ExceptionGroup({KeyError('x'), KeyError('y'), TypeError('b')})
A regular raise would behave similarly:
try:
raise *(ValueError('a'), TypeError('b'))
except *ValueError:
raise KeyError('x')
# would result in:
# ExceptionGroup({KeyError('x'), TypeError('b')})
raise * accepts arguments of type Iterable[BaseException].
Unmatched Exceptions
Example:
try:
raise *(ValueError('a'), TypeError('b'), TypeError('c'), KeyError('e'))
except *ValueError as e:
print(f'got some ValueErrors: {e}')
except *TypeError as e:
print(f'got some TypeErrors: {e}')
raise *e
The above code would print:
got some ValueErrors: ExceptionGroup({ValueError('a')})
got some TypeErrors: ExceptionGroup({TypeError('b'), TypeError('c')})
And then crash with an unhandled KeyError('e') error.
Basically, before interpreting except * clauses, the interpreter will have an exception group object with a list of exceptions in it. Every except * clause, evaluated from top to bottom, can filter some of the exceptions out of the group and process them. In the end, if the exception group has no exceptions left in it, it wold mean that all exceptions were processed. If the exception group has some unprocessed exceptions, the current frame will be "pushed" to the group's traceback and the group would be propagated up the stack.
Exception Chaining
If an error occur during processing a set of exceptions in a except * block, all matched errors would be put in a new ExceptionGroup which would have its __context__ attribute set to the just occurred exception:
try:
raise *(ValueError('a'), ValueError('b'), TypeError('z'))
except *ValueError:
1 / 0
# would result in:
#
# ExceptionGroup({
# TypeError('z'),
# ZeroDivisionError()
# })
#
# where the `ZeroDivizionError()` instance would have
# its __context__ attribute set to
#
# ExceptionGroup({
# ValueError('a'), ValueError('b')
# })
It's also possible to explicitly chain exceptions:
try:
raise *(ValueError('a'), ValueError('b'), TypeError('z'))
except *ValueError as errors:
raise RuntimeError('unexpected values') from errors
# would result in:
#
# ExceptionGroup(
# TypeError('z'),
# RuntimeError('unexpected values')
# )
#
# where the `RuntimeError()` instance would have
# its __cause__ attribute set to
#
# ExceptionGroup({
# ValueError('a'), ValueError('b')
# })
See Also
The design discussed in this issue has been consolidated in https://github.com/python/exceptiongroups/blob/master/except_star.md.
Disclaimer
ExceptionGroupname in this issue, even though there are other alternatives, e.g.AggregateException. Naming of the "exception group" object is outside of the scope of this issue.try..exceptconstruct, shortly called "except*".ValueErrorpropagating through the stack is "naked".ExceptionGroupwould be an iterable object. E.g.list(ExceptionGroup(ValueError('a'), TypeError('b')))would be equal to[ValueError('a'), TypeError('b')]ExceptionGroupwon't be an indexable object; essentially it's similar to Pythonset. The motivation for this is that exceptions can occur in random order, and letting users writegroup[0]to access the "first" error is error prone. The actual implementation ofExceptionGroupwill likely use an ordered list of errors though.ExceptionGroupwill be a subclass ofBaseException, which means it's assignable toException.__context__and can be directly handled withexcept ExceptionGroup.try..exceptwill not be modified.Syntax
We're considering to introduce a new variant of the
try..exceptsyntax to simplify working with exception groups:The new syntax can be viewed as a variant of the tuple unpacking syntax. The
*symbol indicates that zero or more exceptions can be "caught" and processed by oneexcept *clause.We also propose to enable "unpacking" in the
raisestatement:Semantics
Overview
The
except *SpamErrorblock will be run if thetrycode raised anExceptionGroupwith one or more instances ofSpamError. It would also be triggered if a naked instance ofSpamErrorwas raised.The
except *BazError as eblock would aggregate all instances ofBazErrorinto a list, wrap that list into anExceptionGroupinstance, and assign the resultant object toe. The type ofewould beExceptionGroup[BazError]. If there was just one naked instance ofBazError, it would be wrapped into a list and assigned toe.The
except *(BarError, FooError) as ewould aggregate all instances ofBarErrororFooErrorinto a list and assign that wrapped list toe. The type ofewould beExceptionGroup[Union[BarError, FooError]].Even though every
except*star can be called only once, any number of them can be run during handling of anExceptionGroup. E.g. in the above example, bothexcept *SpamError:andexcept *(BarError, FooError) as e:could get executed during handling of oneExceptionGroupobject, or all of theexcept*clauses, or just one of them.It is not allowed to use both regular
exceptclauses and the newexcept*clauses in the sametryblock. E.g. the following example would raise aSyntaxErorr:Exceptions are mached using a subclass check. For example:
could output:
New raise* Syntax
The new
raise *syntax allows to users to only process some exceptions out of the matched set, e.g.:The above code ignores all
EPIPEOS errors, while letting all others propagate.raise *syntax is special: it effectively extends the exception group with a list of errors without creating a newExceptionGroupinstance:A regular raise would behave similarly:
raise *accepts arguments of typeIterable[BaseException].Unmatched Exceptions
Example:
The above code would print:
And then crash with an unhandled
KeyError('e')error.Basically, before interpreting
except *clauses, the interpreter will have an exception group object with a list of exceptions in it. Everyexcept *clause, evaluated from top to bottom, can filter some of the exceptions out of the group and process them. In the end, if the exception group has no exceptions left in it, it wold mean that all exceptions were processed. If the exception group has some unprocessed exceptions, the current frame will be "pushed" to the group's traceback and the group would be propagated up the stack.Exception Chaining
If an error occur during processing a set of exceptions in a
except *block, all matched errors would be put in a newExceptionGroupwhich would have its__context__attribute set to the just occurred exception:It's also possible to explicitly chain exceptions:
See Also
ExceptionGrouptype by @iritkatriel tracked here: GitHub - iritkatriel/cpython at exceptionGroup