Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
This repository was archived by the owner on Apr 10, 2022. It is now read-only.

Commit ef91921

Browse filesBrowse files
authored
Backward compatibility & Design considerations sections (#20)
* edited backwards compatibility section * rewrite 'design consideration' as part of 'rejected ideas' * adoption issues * fixup
1 parent 12cd1f7 commit ef91921
Copy full SHA for ef91921

File tree

1 file changed

+105
-156
lines changed
Filter options

1 file changed

+105
-156
lines changed

‎except_star.md

Copy file name to clipboardExpand all lines: except_star.md
+105-156Lines changed: 105 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ The purpose of this PEP, then, is to add the `except*` syntax for handling
9494
`ExceptionGroups`s in the interpreter, which in turn requires that
9595
`ExceptionGroup` is added as a builtin type. The semantics of handling
9696
`ExceptionGroup`s are not backwards compatible with the current exception
97-
handling semantics, so we could not modify the behaviour of the `except`
98-
keyword and instead added the new `except*` syntax.
97+
handling semantics, so we are not proposing to modify the behaviour of the
98+
`except` keyword but rather to add the new `except*` syntax.
9999

100100

101101
## Specification
@@ -822,167 +822,33 @@ def foo():
822822
raise TypeError("Can't have B without A!") from e
823823
```
824824

825-
## Design Considerations
826-
827-
### Why try..except* syntax
828-
829-
Fundamentally there are two kinds of exceptions: *control flow exceptions*
830-
(e.g. `KeyboardInterrupt` or `asyncio.CancelledError`) and
831-
*operation exceptions* (e.g. `TypeError` or `KeyError`).
832-
833-
When writing async/await code that uses a concept of TaskGroups (or Trio's
834-
nurseries) to schedule different code concurrently, the users should
835-
approach these two kinds in a fundamentally different way.
836-
837-
*Operation exceptions* such as `KeyError` should be handled within
838-
the async Task that runs the code. E.g. this is what users should do:
839-
840-
```python
841-
try:
842-
dct[key]
843-
except KeyError:
844-
# handle the exception
845-
```
846-
847-
and this is what they shouldn't do:
848-
849-
```python
850-
try:
851-
async with asyncio.TaskGroup() as g:
852-
g.create_task(task1); g.create_task(task2)
853-
except *KeyError:
854-
# handling KeyError here is meaningless, there's
855-
# no context to do anything with it but to log it.
856-
```
857-
858-
*Control flow exceptions* are different. If, for example, we want to
859-
cancel an asyncio Task that spawned other multiple concurrent Tasks in it
860-
with a an `asyncio.TaskGroup`, the following will happen:
861-
862-
* CancelledErrors will be propagated to all still running tasks within
863-
the group;
864-
865-
* CancelledErrors will be propagated to the Task that scheduled the group and
866-
bubble up from `async with TaskGroup()`;
867-
868-
* CancelledErrors will be propagated to the outer Task until either the entire
869-
program shuts down with a `CancelledError`, or the cancellation is handled
870-
and silenced (e.g. by `asyncio.wait_for()`).
871-
872-
*Control flow exceptions* alter the execution flow of a program.
873-
Therefore it is sometimes desirable for the user to react to them and
874-
run code, for example, to free resources.
875-
876-
Suppose we have the `except *ExceptionType` syntax that only matches
877-
`ExceptionGroup[ExceptionType]` exceptions (a naked `ExceptionType` wouldn't
878-
be matched). This means that we'd see a lot of code duplication:
879-
880-
881-
```python
882-
try:
883-
async with asyncio.TaskGroup() as g:
884-
g.create_task(task1); g.create_task(task2)
885-
except *CancelledError:
886-
log('cancelling server bootstrap')
887-
await server.stop()
888-
raise
889-
except CancelledError:
890-
# Same code, really.
891-
log('cancelling server bootstrap')
892-
await server.stop()
893-
raise
894-
```
895-
896-
Which leads to the conclusion that `except *CancelledError as e` should both:
897-
898-
* catch a naked `CancelledError`, wrap it in an `ExceptionGroup` and bind it
899-
to `e`. The type of `e` would always be `ExceptionGroup[CancelledError]`.
900-
901-
* if an exception group is propagating through the `try`,
902-
`except *CancelledError` should split the group and handle all exceptions
903-
at once with one run of the code in `except *CancelledError` (and not
904-
run the code for every matched individual exception.)
905-
906-
Why "handle all exceptions at once"? Why not run the code in the except
907-
clause for every matched exception that we have in the group?
908-
Basically because there's no need to. As we mentioned above, catching
909-
*operation exceptions* should be done with the regular `except KeyError`
910-
within the Task boundary, where there's context to handle a `KeyError`.
911-
Catching *control flow exceptions* is needed to **react** to a global
912-
signal, do cleanup or logging, but ultimately to either **stop** the signal
913-
**or propagate** it up the caller chain.
914-
915-
Separating exception kinds to two distinct groups (operation & control flow)
916-
leads to another conclusion: an individual `try..except` block usually handles
917-
either the former or the latter, **but not a mix of both**. Which leads to the
918-
conclusion that `except *CancelledError` should switch the behavior of the
919-
entire `try` block to make it run several of its `except*` clauses if
920-
necessary. Therefore:
921-
922-
```python
923-
try:
924-
# code
925-
except KeyError:
926-
# handle
927-
except ValueError:
928-
# handle
929-
```
825+
## Backwards Compatibility
930826

931-
is a regular `try..except` block to be used for reacting to
932-
*operation exceptions*. And:
827+
Backwards compatibility was a requirement of our design, and the changes we
828+
propose in this PEP will not break any existing code:
933829

934-
```python
935-
try:
936-
# code
937-
except *TimeoutError:
938-
# handle
939-
except *CancelledError:
940-
# handle
941-
```
830+
* The addition of a new builtin exception type `ExceptionGroup` does not impact
831+
existing programs. The way that existing exceptions are handled and displayed
832+
does not change in any way.
942833

943-
is an entirely different construct meant to make it easier to react to
944-
*control flow* signals. When specified that way, it is expected from the user
945-
standpoint that both `except` clauses can be potentially run.
834+
* The behaviour of `except` is unchanged so existing code will continue to work.
835+
Programs will only be impacted by the changes proposed in this PEP once they
836+
begin to use `ExceptionGroup`s and `except*`.
946837

947-
Lastly, code that combines handling of both operation and control flow
948-
exceptions is unrealistic and impractical, e.g.:
949838

950-
```python
951-
try:
952-
async with TaskGroup() as g:
953-
g.create_task(task1())
954-
g.create_task(task2())
955-
except ValueError:
956-
# handle ValueError
957-
except *CancelledError:
958-
# handle cancellation
959-
raise
960-
```
839+
Once programs begin to use these features, there will be migration issues to
840+
consider:
961841

962-
In the above snippet it is impossible to attribute which task raised a
963-
`ValueError` -- `task1` or `task2`. So it really should be handled directly
964-
in those tasks. Whereas handling `*CancelledError` makes sense -- it means that
965-
the current task is being canceled and this might be a good opportunity to do
966-
a cleanup.
842+
* An `except Exception:` clause will not catch `ExceptionGroup`s because they
843+
are derived from `BaseException`. Any such clause will need to be replaced
844+
by `except (Exception, ExceptionGroup):` or `except *Exception:`.
967845

968-
## Backwards Compatibility
846+
* Similarly, any `except T:` clause that wraps code which is now potentially
847+
raising `ExceptionGroup` needs to become `except *T:`, and its body may need
848+
to be updated.
969849

970-
The behaviour of `except` is unchanged so existing code will continue to work.
971-
972-
### Adoption of try..except* syntax
973-
974-
Application code typically can dictate what version of Python it requires.
975-
Which makes introducing TaskGroups and the new `except*` clause somewhat
976-
straightforward. Upon switching to Python 3.10, the application developer
977-
can grep their application code for every *control flow* exception they handle
978-
(search for `except CancelledError`) and mechanically change it to
979-
`except *CancelledError`.
980-
981-
Library developers, on the other hand, will need to maintain backwards
982-
compatibility with older Python versions, and therefore they wouldn't be able
983-
to start using the new `except*` syntax right away. They will have to use
984-
the new ExceptionGroup low-level APIs along with `try..except ExceptionGroup`
985-
to support running user code that can raise exception groups.
850+
* Libraries that need to support older python versions will not be able to use
851+
`except*` or raise `ExceptionGroup`s.
986852

987853

988854
## Security Implications
@@ -1031,14 +897,97 @@ would be too confusing for users at this time, so it is more appropriate
1031897
to introduce the `except*` syntax for `ExceptionGroup`s while `except`
1032898
continues to be used for simple exceptions.
1033899

900+
### Applying an `except*` clause on one exception at a time
901+
902+
We considered making `except*` clauses always execute on a single exception,
903+
possibly executing the same clause multiple times when it matches multiple
904+
exceptions. We decided instead to execute each `except*` clause at most once,
905+
giving it an `ExceptionGroup` that contains all matching exceptions. The reason
906+
for this decision was the observation that when a program needs to know the
907+
patricular context of an exception it is handling, it handles it before
908+
grouping it with other exceptions and raising them together.
909+
910+
For example, `KeyError` is an exception that typically relates to a certain
911+
operation. Any recovery code would be local to the place where the error
912+
occurred, and would use the traditional `except`:
913+
914+
```python
915+
try:
916+
dct[key]
917+
except KeyError:
918+
# handle the exception
919+
```
920+
921+
It is unlikely that asyncio users would want to do something like this:
922+
923+
```python
924+
try:
925+
async with asyncio.TaskGroup() as g:
926+
g.create_task(task1); g.create_task(task2)
927+
except *KeyError:
928+
# handling KeyError here is meaningless, there's
929+
# no context to do anything with it but to log it.
930+
```
931+
932+
When a program handles a collection of exceptions that were aggregated into
933+
an exception group, it would not typically attempt to recover from any
934+
particular failed operation, but will rather use the types of the errors to
935+
determine how they should impact the program's control flow or what logging
936+
or cleanup is required. This decision is likely to be the same whether the group
937+
contains a single or multiple instances of something like a `KeyboardInterrupt`
938+
or `asyncio.CancelledError`. Therefore, it is more convenient to handle all
939+
exceptions matching an `except*` at once. If it does turn out to be necessary,
940+
the handler can inpect the `ExceptionGroup` and process the individual
941+
exceptions in it.
942+
943+
### Not matching naked exceptions in `except*`
944+
945+
We considered the option of making `except *T` match only `ExceptionGroup`s
946+
that contain `T`s, but not naked `T`s. To see why we thought this would not be a
947+
desirable feature, return to the distinction in the previous paragraph between
948+
operation errors and control flow exceptions. If we don't know whether
949+
we should expect naked exceptions or `ExceptionGroup`s from the body of a
950+
`try` block, then we're not in the position of handling operation errors.
951+
Rather, we are likely calling some callback and will be handling errors to make
952+
control flow decisions. We are likely to do the same thing whether we catch a
953+
naked exception of type `T` or an `ExceptionGroup` with one or more `T`s.
954+
Therefore, the burden of having to explicitly handle both is not likely to have
955+
semantic benefit.
956+
957+
If it does turn out to be necessary to make the distinction, it is always
958+
possible to nest in the `try-except*` clause an additional `try-except` clause
959+
which intercepts and handles a naked exception before the `except*` clause
960+
has a change to wrap it in an `ExceptionGroup`. In this case the overhead
961+
of specifying both is not addition burden - we really do need to write a
962+
separate code block to handle each case:
963+
964+
```python
965+
try:
966+
try:
967+
...
968+
except SomeError:
969+
# handle the naked exception
970+
except *SomeError:
971+
# handle the ExceptionGroup
972+
```
973+
974+
### Allow mixing `except:` and `except*:` in the same `try`
975+
976+
This option was rejected because it adds complexity without adding useful
977+
semantics. Presumably the intention would be that an `except T:` block handles
978+
only naked exceptions of type `T`, while `except *T:` handles `T` in
979+
`ExceptionGroup`s. We already discussed above why this is unlikely
980+
to be useful in practice, and if it is needed then the nested `try-except`
981+
block can be used instead to achieve the same result.
982+
1034983

1035984
## See Also
1036985

1037986
* An analysis of how exception groups will likely be used in asyncio
1038987
programs:
1039988
https://github.com/python/exceptiongroups/issues/3#issuecomment-716203284
1040989

1041-
* The issue where this concept was first formalized:
990+
* The issue where the `except*` concept was first formalized:
1042991
https://github.com/python/exceptiongroups/issues/4
1043992

1044993

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.