__exit__
must accept 3 arguments: type, value, traceback¶
A contextmanager
class is any class that implements the __enter__
and __exit__
methods according to the Python Language Reference’s context management protocol. Implementing the context management protocol enables you to use the with
statement with instances of the class. The with
statement is used to ensure that setup and teardown operations are always executed before and after a given block of code. It is functionally equivalent to try...finally
blocks, except that with
statements are more concise.
For example, the following block of code using a with
statement…
with EXPRESSION:
BLOCK
… is equivalent to the following block of code using try
and finally
statements.
EXPRESSION.__enter__()
try:
BLOCK
finally:
EXPRESSION.__exit__(exception_type, exception_value, traceback)
In order for __exit__
to work properly it must have exactly three arguments: exception_type
, exception_value
, and traceback
. The formal argument names in the method definition do not need to correspond directly to these names, but they must appear in this order. If any exceptions occur while attempting to execute the block of code nested after the with
statement, Python will pass information about the exception into the __exit__
method. You can then modify the definition of __exit__
to gracefully handle each type of exception.
Anti-pattern¶
The __exit__
method defined in the Rectangle
class below does not conform to Python’s context management protocol. The method is supposed to take four arguments: self
, exception type, exception value, and traceback. Because the method signature does not match what Python expects, __exit__
is never called even though it should have been, because the method divide_by_zero
creates a ZeroDivisionError
exception.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def __enter__(self):
print("in __enter__")
return self
def __exit__(self):
# never called because
# argument signature is wrong
print("in __exit__")
def divide_by_zero(self):
# causes ZeroDivisionError exception
return self.width / 0
with Rectangle(3, 4) as r:
r.divide_by_zero()
# __exit__ should be called but isn't
# Output:
# "in __enter__"
# Traceback (most recent call last):
# File "e0235.py", line 27, in <module>
# r.divide_by_zero()
# TypeError: __exit__() takes exactly 1 argument (4 given)
Best practices¶
Modifying __exit__
to accept four arguments ensures that __exit__
is properly called when an exception is raised in the indented block of code following the with
statement. Note that the argument names do not have to exactly match the names provided below. But they must occur in the order provided below.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def __enter__(self):
print("in __enter__")
return self
def __exit__(self, exception_type, exception_value, traceback):
print("in __exit__")
def divide_by_zero(self):
# causes ZeroDivisionError exception
return self.width / 0
with Rectangle(3, 4) as r:
# exception successfully pass to __exit__
r.divide_by_zero()
# Output:
# "in __enter__"
# "in __exit__"
# Traceback (most recent call last):
# File "e0235.py", line 27, in <module>
# r.divide_by_zero()
References¶
PyLint - E0235,unexpected-special-method-signature