Classes have a defineable function __exit__
that allows implementation of a context manager.
It takes the required arguments:
def __exit__(self, exc_type, exc_val, exc_tb):
but I cannot find a definitive definition of what those arguments are and their types.
Here's my best guess of what they are and why, but I'm not entirely sure:
def __exit__(self, exc_type: Exception, exc_val: TracebackException, exc_tb: TracebackType):
Python defines a TracebackException
class that accepts an exc_type
argument which is used contextually in the constructor within issubclass
with SyntaxError
, which infers that exc_type
is indeed some sort of Exception
, which SyntaxError
inherits from.
Also, in that TracebackException
class is an exc_value
argument that matches up to our exc_val
which seems to have various attributes like __cause__
, __context__
, and other attributes that are all defined in TracebackType
itself. This makes me think that the parameter is itself an instance of TracebackException
.
Python defines a walk_tb function that uses exc_tb
as an argument (manually traced from docs.python.org), and this object appears to have tb_frame
, tb_lineno
, and tb_next
attributes which can be traced back to a TracebackType
class in the typeshed
library.
Thoughts?
Official stubs can be found in typeshed/stdlib/contextlib.pyi
on GitHub.
exc_type
is the exception's class. exc_val
is the exception instance. exc_tb
is a traceback object, of which there is a reference in types.TracebackType
.
In general it should be the case that
type(exc_val) is exc_type
exc_val.__traceback__ is exc_tb
Note that __exit__
is still invoked when there was no exception raised by the code under a context manager, and the args will be (None, None, None)
so all three arguments should be annotated optional.
The return type is boolean, but it is common for context managers to return None
, expecting that to be handled similarly as False
, so this return is also optional.
Then a modern and correct annotation for it should look something like this:
from types import TracebackType
def __exit__(
self,
type_: type[BaseException] | None,
value: BaseException | None,
traceback: TracebackType | None,
) -> bool | None: ...
I'm using the syntax recommendations as described in Typing Best Practices: Stylistic Practices.
For older Python versions where you may not have a subscriptable type
(< 3.9), or a union operator for types (< 3.10), you can use from __future__ import annotations
and only run your type-checker on 3.10+, or you can write a more widely compatible format:
from types import TracebackType
from typing import Optional, Type
def __exit__(
self,
exctype: Optional[Type[BaseException]],
excinst: Optional[BaseException],
exctb: Optional[TracebackType],
) -> Optional[bool]: ...
You might wonder why this API has three arguments when two of them can be trivially determined from the exception instance itself. But it wasn't always that way, in older versions of Python you could raise strings as exceptions, and the exception's __traceback__
attribute wasn't there until Python 2.5.
The three-argument exit remains for now because there weren't convincing enough reasons to improve it. Simplifying the exit signature to def __exit__(self, exc)
was proposed for Python 3.12 in
PEP 707 – A simplified signature for __exit__
and __aexit__
, but that PEP was rejected.