@@ -94,8 +94,8 @@ The purpose of this PEP, then, is to add the `except*` syntax for handling
94
94
` ExceptionGroups ` s in the interpreter, which in turn requires that
95
95
` ExceptionGroup ` is added as a builtin type. The semantics of handling
96
96
` 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.
99
99
100
100
101
101
## Specification
@@ -822,167 +822,33 @@ def foo():
822
822
raise TypeError (" Can't have B without A!" ) from e
823
823
```
824
824
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
930
826
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 :
933
829
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.
942
833
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* ` .
946
837
947
- Lastly, code that combines handling of both operation and control flow
948
- exceptions is unrealistic and impractical, e.g.:
949
838
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:
961
841
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: ` .
967
845
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.
969
849
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.
986
852
987
853
988
854
## Security Implications
@@ -1031,14 +897,97 @@ would be too confusing for users at this time, so it is more appropriate
1031
897
to introduce the ` except* ` syntax for ` ExceptionGroup ` s while ` except `
1032
898
continues to be used for simple exceptions.
1033
899
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
+
1034
983
1035
984
## See Also
1036
985
1037
986
* An analysis of how exception groups will likely be used in asyncio
1038
987
programs:
1039
988
https://github.com/python/exceptiongroups/issues/3#issuecomment-716203284
1040
989
1041
- * The issue where this concept was first formalized:
990
+ * The issue where the ` except* ` concept was first formalized:
1042
991
https://github.com/python/exceptiongroups/issues/4
1043
992
1044
993
0 commit comments