40

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):

exc_type

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.

exc_val

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.

exc_tb

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?

notacorn
  • 3,526
  • 4
  • 30
  • 60

2 Answers2

49

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.

Then a correct annotation for it should look something like this:

def __exit__(self, exctype: Optional[Type[BaseException]],
             excinst: Optional[BaseException],
             exctb: Optional[TracebackType]) -> 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. And you can still raise old-style classes as exceptions in Python 2.7 (!)

wim
  • 338,267
  • 99
  • 616
  • 750
  • 1
    From where do we import TrackbackType? – Mentos1386 Apr 02 '20 at 13:30
  • 3
    @Mentos1386 Import it from the `types` module, not to be confused with the `typing` module. – Gigi Bayte 2 Aug 06 '20 at 15:36
  • 1
    Should the return type annotation of __exit__ be `Optional[bool]`? https://www.python.org/dev/peps/pep-0343/#id38 – Laurenz Oct 28 '20 at 18:06
  • @Laurenz typeshed used bool https://github.com/python/typeshed/blob/095464874accef1cc5303f325ccabe88756bcd8b/stdlib/2and3/contextlib.pyi#L70 I guess there’s no practical difference with returning None instead of False, but it just works because of duck typing. – wim Nov 21 '20 at 17:44
  • If you don't want to handle the exception then `def __exit__(self, *exc: List[Any]) -> None:` seems to do the job (Mypy doesn't complain any more). – starblue Aug 14 '23 at 14:18
5

In mypy issue 4885, Jelle Zijlstra provides the standard signature for __exit__.

Adapted to your argument names and the appropriate imports:

from typing import Optional, Type
from types import TracebackType

def __exit__(
    self,
    exc_type: Optional[Type[BaseException]],
    exc_val: Optional[BaseException],
    exc_tb: Optional[TracebackType],
) -> bool:
   ...

You should return True from __exit__ if you want to suppress an exception raised in the context and False in all other cases.

Daniel Walker
  • 6,380
  • 5
  • 22
  • 45
Anthon
  • 69,918
  • 32
  • 186
  • 246