I have the following code:
class Test:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f'entering {self.name}')
def __exit__(self, exctype, excinst, exctb) -> bool:
print(f'exiting {self.name}')
return True
with Test('first') as test:
print(f'in {test.name}')
test = Test('second')
with test:
print(f'in {test.name}')
Running it produces the following output:
entering first
exiting first
entering second
in second
exiting second
But I expected it to produce:
entering first
in first
exiting first
entering second
in second
exiting second
Why isn't the code within my first example called?
The __enter__ method should return the context object. with ... as ... uses the return value of __enter__ to determine what object to give you. Since your __enter__ returns nothing, it implicitly returns None, so test is None.
with Test('first') as test:
print(f'in {test.name}')
test = Test('second')
with test:
print(f'in {test.name}')
So test is none. Then test.name is an error. That error gets raised, so Test('first').__exit__ gets called. __exit__ returns True, which indicates that the error has been handled (essentially, that your __exit__ is acting like an except block), so the code continues after the first with block, since you told Python everything was fine.
Consider
def __enter__(self):
print(f'entering {self.name}')
return self
You might also consider not returning True from __exit__ unless you truly intend to unconditionally suppress all errors in the block (and fully understand the consequences of suppressing other programmers' errors, as well as KeyboardInterrupt, StopIteration, and various system signals)
I believe this behavior is because __enter__ must return something that will be operated on, that in this case will be accessed with the name test. By changing __enter__ to the following
def __enter__(self):
print(f"entering {self.name}")
return self
we get the expected behavior.
The problem is that your __enter__ method returns None. Hence, test is assigned None.
Then you try to access (None).name, which raises an error. Since your __exit__ method returns True always, it will suppress any errors. According to the docs:
Returning a true value from this method will cause the with statement to suppress the exception and continue execution with the statement immediately following the with statement.
__enter__ is giving None as an output, since there is no return, therefore it would directly trigger __exit__ since None has no attribute name, ex:
>>> None.name
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
None.__name__
AttributeError: 'NoneType' object has no attribute 'name'
>>>
If you set it to call __class__.__name__ (None objects have that attribute, which gives NoneType), you could find the problem easily:
class Test:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f'entering {self.name}')
def __exit__(self, exctype, excinst, exctb) -> bool:
print(f'exiting {self.name}')
return True
with Test('first') as test:
print(f'in {test.__class__.__name__}')
test = Test('second')
with test:
print(f'in {test.__class__.__name__}')
Output:
entering first
in NoneType
exiting first
entering second
in Test
exiting second
As you can see, it says in NoneType, not returning any value is the reason to this. In a lot of cases, __enter__ doesn't need to return, but in this case the Test class needs it to return.
The solution would be to keep the Test instance, so that it calls the name of a returned self after the context manager __enter__ result. So far the __enter__ results None, therefore None.name attribute doesn't exist. So if you return self, test.name attribute would exist.
The solution would be to return self in the __enter__ magic method implementation:
...
def __enter__(self):
print(f'entering {self.name}')
return self
...
Full code:
class Test:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f'entering {self.name}')
return self
def __exit__(self, exctype, excinst, exctb) -> bool:
print(f'exiting {self.name}')
return True
with Test('first') as test:
print(f'in {test.name}')
test = Test('second')
with test:
print(f'in {test.name}')
Output:
entering first
in first
exiting first
entering second
in second
exiting second
The extra info I gave that the other answers didn't give is a more concrete proof of the __enter__ method implementation giving None. I showed an example as well of it.
The reason is that the first and second cases don't do the same.
In first:
__init__;with calls __enter__;as stores the result of __enter__ into test__enter__ doesn't have a return value, test is None.In second:
__init__;test;with calls __enter__;__enter__;test keeps referring to the object that was created originally.In both cases, __exit__ is called for the object that with is handling, so you see the right label being printed; it's just that, in first, the test identifier is not bound to that same object.
NB __enter__ doesn't have to return self. It might return something else entirely, for instance you may open a file and make __enter__ return the stream, while __exit__ may close it. If it were a given that __enter__ should return self, that would be redundant and could just be implied.