diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 12d7d8abb26504..f90b6761154af5 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -851,6 +851,78 @@ The following exceptions are used as warning categories; see the .. versionadded:: 3.2 +Exception groups +---------------- + +The following are used when it is necessary to raise multiple unrelated +exceptions. They are part of the exception hierarchy so they can be +handled with :keyword:`except` like all other exceptions. In addition, +they are recognised by :keyword:`except*`, which matches +their subgroups based on the types of the contained exceptions. + +.. exception:: ExceptionGroup(msg, excs) +.. exception:: BaseExceptionGroup(msg, excs) + + Both of these exception types wrap the exceptions in the sequence ``excs``. + The ``msg`` parameter must be a string. The difference between the two + classes is that :exc:`BaseExceptionGroup` extends :exc:`BaseException` and + it can wrap any exception, while :exc:`ExceptionGroup` extends :exc:`Exception` + and it can only wrap subclasses of :exc:`Exception`. This design is so that + ``except Exception`` catches an :exc:`ExceptionGroup` but not + :exc:`BaseExceptionGroup`. + + The :exc:`BaseExceptionGroup` constructor returns an :exc:`ExceptionGroup` + rather than a :exc:`BaseExceptionGroup` if all contained exceptions are + :exc:`Exception` instances, so it can be used to make the selection + automatic. The :exc:`ExceptionGroup` constructor, on the other hand, + raises a :exc:`TypeError` if any contained exception is not an + :exc:`Exception` subclass. + + .. method:: subgroup(condition) + + Returns an exception group that contains only the exceptions from the + current group that match *condition*, or ``None`` if the result is empty. + + The condition can be either a function that accepts an exception and returns + true for those that should be in the subgroup, or it can be an exception type + or a tuple of exception types, which is used to check for a match using the + same check that is used in an ``except`` clause. + + The nesting structure of the current exception is preserved in the result, + as are the values of its :attr:`message`, :attr:`__traceback__`, + :attr:`__cause__`, :attr:`__context__` and :attr:`__note__` fields. + Empty nested groups are omitted from the result. + + The condition is checked for all exceptions in the nested exception group, + including the top-level and any nested exception groups. If the condition is + true for such an exception group, it is included in the result in full. + + .. method:: split(condition) + + Like :meth:`subgroup`, but returns the pair ``(match, rest)`` where ``match`` + is ``subgroup(condition)`` and ``rest`` is the remaining non-matching + part. + + .. method:: derive(excs) + + Returns an exception group with the same :attr:`message`, + :attr:`__traceback__`, :attr:`__cause__`, :attr:`__context__` + and :attr:`__note__` but which wraps the exceptions in ``excs``. + + This method is used by :meth:`subgroup` and :meth:`split`. A + subclass needs to override it in order to make :meth:`subgroup` + and :meth:`split` return instances of the subclass rather + than :exc:`ExceptionGroup`. :: + + >>> class MyGroup(ExceptionGroup): + ... def derive(self, exc): + ... return MyGroup(self.message, exc) + ... + >>> MyGroup("eg", [ValueError(1), TypeError(2)]).split(TypeError) + (MyGroup('eg', [TypeError(2)]), MyGroup('eg', [ValueError(1)])) + + .. versionadded:: 3.11 + Exception hierarchy ------------------- diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index cf8ad1787b2915..12fac9ed9bd193 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -220,6 +220,7 @@ returns the list ``[0, 1, 2]``. .. _try: .. _except: +.. _except_star: .. _finally: The :keyword:`!try` statement @@ -237,12 +238,16 @@ The :keyword:`try` statement specifies exception handlers and/or cleanup code for a group of statements: .. productionlist:: python-grammar - try_stmt: `try1_stmt` | `try2_stmt` + try_stmt: `try1_stmt` | `try2_stmt` | `try3_stmt` try1_stmt: "try" ":" `suite` : ("except" [`expression` ["as" `identifier`]] ":" `suite`)+ : ["else" ":" `suite`] : ["finally" ":" `suite`] try2_stmt: "try" ":" `suite` + : ("except" "*" `expression` ["as" `identifier`] ":" `suite`)+ + : ["else" ":" `suite`] + : ["finally" ":" `suite`] + try3_stmt: "try" ":" `suite` : "finally" ":" `suite` @@ -325,6 +330,47 @@ when leaving an exception handler:: >>> print(sys.exc_info()) (None, None, None) +.. index:: + keyword: except_star + +The :keyword:`except*` clause(s) are used for handling +:exc:`ExceptionGroup`s. The exception type for matching is interpreted as in +the case of :keyword:`except`, but in the case of exception groups we can have +partial matches when the type matches some of the exceptions in the group. +This means that multiple except* clauses can execute, each handling part of +the exception group. Each clause executes once and handles an exception group +of all matching exceptions. Each exception in the group is handled by at most +one except* clause, the first that matches it. :: + + >>> try: + ... raise ExceptionGroup("eg", + ... [ValueError(1), TypeError(2), OSError(3), OSError(4)]) + ... except* TypeError as e: + ... print(f'caught {type(e)} with nested {e.exceptions}') + ... except* OSError as e: + ... print(f'caught {type(e)} with nested {e.exceptions}') + ... + caught with nested (TypeError(2),) + caught with nested (OSError(3), OSError(4)) + + Exception Group Traceback (most recent call last): + | File "", line 2, in + | ExceptionGroup: eg + +-+---------------- 1 ---------------- + | ValueError: 1 + +------------------------------------ + >>> + + Any remaining exceptions that were not handled by any except* clause + are re-raised at the end, combined into an exception group along with + all exceptions that were raised from within except* clauses. + + An except* clause must have a matching type, and this type cannot be a + subclass of :exc:`BaseExceptionGroup`. It is not possible to mix except + and except* in the same :keyword:`try`. :keyword:`break`, + :keyword:`continue` and :keyword:`return` cannot appear in an except* + clause. + + .. index:: keyword: else statement: return diff --git a/Doc/tutorial/errors.rst b/Doc/tutorial/errors.rst index f2490d65db5d49..33c6a608802127 100644 --- a/Doc/tutorial/errors.rst +++ b/Doc/tutorial/errors.rst @@ -496,3 +496,92 @@ used in a way that ensures they are always cleaned up promptly and correctly. :: After the statement is executed, the file *f* is always closed, even if a problem was encountered while processing the lines. Objects which, like files, provide predefined clean-up actions will indicate this in their documentation. + + +.. _tut-exception-groups: + +Raising and Handling Multiple Unrelated Exceptions +================================================== + +There are situations where it is necessary to report several exceptions that +have occurred. This it often the case in concurrency frameworks, when several +tasks may have failed in parallel, but there are also other use cases where +it is desirable to continue execution and collect multiple errors rather than +raise the first exception. + +The builtin :exc:`ExceptionGroup` wraps a list of exception instances so +that they can be raised together. It is an exception itself, so it can be +caught like any other exception. :: + + >>> def f(): + ... excs = [OSError('error 1'), SystemError('error 2')] + ... raise ExceptionGroup('there were problems', excs) + ... + >>> f() + + Exception Group Traceback (most recent call last): + | File "", line 1, in + | File "", line 3, in f + | ExceptionGroup: there were problems + +-+---------------- 1 ---------------- + | OSError: error 1 + +---------------- 2 ---------------- + | SystemError: error 2 + +------------------------------------ + >>> try: + ... f() + ... except Exception as e: + ... print(f'caught {type(e)}: e') + ... + caught : e + >>> + +By using ``except*`` instead of ``except``, we can selectively +handle only the exceptions in the group that match a certain +type. In the following example, which shows a nested exception +group, each ``except*`` clause extracts from the group exceptions +of a certain type while letting all other exceptions propagate to +other clauses and eventually to be reraised. :: + + >>> def f(): + ... raise ExceptionGroup("group1", + ... [OSError(1), + ... SystemError(2), + ... ExceptionGroup("group2", + ... [OSError(3), RecursionError(4)])]) + ... + >>> try: + ... f() + ... except* OSError as e: + ... print("There were OSErrors") + ... except* SystemError as e: + ... print("There were SystemErrors") + ... + There were OSErrors + There were SystemErrors + + Exception Group Traceback (most recent call last): + | File "", line 2, in + | File "", line 2, in f + | ExceptionGroup: group1 + +-+---------------- 1 ---------------- + | ExceptionGroup: group2 + +-+---------------- 1 ---------------- + | RecursionError: 4 + +------------------------------------ + >>> + +Note that the exceptions nested in an exception group must be instances, +not types. This is because in practice the exceptions would typically +be ones that have already been raised and caught by the program, along +the following pattern:: + + >>> excs = [] + ... for test in tests: + ... try: + ... test.run() + ... except Exception as e: + ... excs.append(e) + ... + >>> if excs: + ... raise ExceptionGroup("Test Failures", excs) + ... +