Clarifying typing behavior for exception handlers (except/except*)

I’m piecing together what behavior a typechecker should implement for except and except* based on the docs and the typing spec, and there’s a few things that I’m hoping to clarify here.

In 8. Errors and Exceptions — Python 3.12.3 documentation, the tutorial incorrectly says that only tuple literals are allowed in except clauses:

An except clause may name multiple exceptions as a parenthesized tuple

If this is intentional to avoid confusing beginners then never mind, but if it’s a mistake then I think the “parenthesized” should be removed.

In 8. Compound statements — Python 3.12.3 documentation, the following correctly indicates that it could be any tuple, not just a literal:

For an except clause with an expression, that expression is evaluated, and the clause matches the exception if the resulting object is “compatible” with the exception. An object is compatible with an exception if the object is the class or a non-virtual base class of the exception object, or a tuple containing an item that is the class or a non-virtual base class of the exception object.

In the except* section that immediately follows, it mentions that the type cannot be BaseExceptionGroup but refers to the matching type as singular, without mentioning whether tuples are allowed.

An except* clause must have a matching type, and this type cannot be a subclass of BaseExceptionGroup.

The typing spec doesn’t seem to have anything about exception handler typechecking in the exceptions section. (no link provided since I’m limited to 2 links per post due to being new here)

I have 3 questions that aren’t clearly answered by the docs above:

  1. Should except* allow tuples of types?

Mypy/pyright/runtime all allow it, so this seems allowed and maybe we should clarify the doc here.

  1. Should typecheckers check against types that extend BaseExceptionGroup in except*?

The runtime gives TypeError: catching ExceptionGroup with except* is not allowed. Use except instead. when executing except* ExceptionGroup:. Mypy also gives an error, but Pyright allows it.

I think this should be checked against, and maybe the docs for except* should mention the “Use except instead” part.

  1. If yes for #2, should the type system disallow values which could be BaseExceptionGroup (like a type variable of type[BaseException]) in except*?

For example, something like this would pass mypy but give an error at runtime.

exception_type: type[BaseException] = BaseExceptionGroup
try:
    raise ExceptionGroup("eg", [ValueError(1)])
except* exception_type:
    print("foo")

This is an edge case so maybe it’s better left up to implementors to decide how conservative/best-effort they want, but I wanted to bring it up to hear everyone’s opinions.

Seems like a few things we could add/update in the language reference and/or the typing spec to make things clearer, I’m happy to contribute those updates once we all agree.

2 Likes

Welcome, and thanks for the detailed post!

I think the “parenthesized” part here is because except A, B: is a SyntaxError, to avoid confusion with the old Python 2 syntax (where you wrote except Exception, e: instead of except Exception as e:).

Possibly this wording can be improved, but I wouldn’t look to the tutorial for a fully precise specification of the behavior in rarely seen cases. You also link the wording in the language reference, and there the wording appears to be mostly correct.

I agree this is imprecise and should mention that tuples are allowed. Could you open an issue and make a PR to the language reference?

I don’t want the typing spec to duplicate the language reference; in cases like this, the implicit spec is that type checkers should follow the behavior specified in the language reference.

I think ideally type checkers should give an error if the except* clause contains a literal class or tuple of literal classes and one of them is a subclass of ExceptionGroup, yes.

Note your example uses a type alias, not a type variable.

I think disallowing your example would be more trouble than it’s worth. The type system has no way to express difference types.

2 Likes

I’ve added this missing check in pyright. It’s included in the latest version (pyright 1.1.367).

Code sample in pyright playground

3 Likes

Makes sense to me, thanks for the detailed response!

I agree this is imprecise and should mention that tuples are allowed. Could you open an issue and make a PR to the language reference?

I have filed an issue and created a PR: