51

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?

U13-Forward
  • 69,221
  • 14
  • 89
  • 114
Ian Newson
  • 7,679
  • 2
  • 47
  • 80

5 Answers5

60

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)

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • 2
    Interestingly, the original PEP-310 proposal for `with` behaved as the OP expected. This was changed in PEP-343. I wonder why they didn't make the object the default context when `__enter__` returns `None`, since that's such a common case. – Barmar Sep 17 '21 at 15:33
  • 5
    @Barmar: Sometimes `__enter__` returns `None` because the `as` target really should be set to `None`. If a return value of `None` was special-cased, that would break cases where `None` really is desired. Saving a few `return self` lines isn't worth that. – user2357112 Sep 18 '21 at 10:22
  • (And no, I don't mean an `__enter__` method with a plain `return None`. I'm talking about cases like if `__enter__` returns the value of some state variable that's allowed to be `None`, or if it returns the result of some computation whose output could be `None`.) – user2357112 Sep 18 '21 at 10:38
  • @user2357112supportsMonica Wouldn't that cause an error when it later tries to call `None.__exit__()`? – Barmar Sep 19 '21 at 00:13
  • 4
    @Barmar: No. `with` calls the context manager's `__exit__`, not the `__enter__` return value's `__exit__`. – user2357112 Sep 19 '21 at 00:32
10

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.

juanpa.arrivillaga
  • 88,713
  • 10
  • 131
  • 172
4

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.

NGilbert
  • 485
  • 6
  • 12
  • 4
    `__enter__` doesn't *have* to return a value, but `Test` is used in a way that *assumes* it will. – chepner Sep 17 '21 at 13:36
3

The reason is that the first and second cases don't do the same.

In first:

  • the object is created, which calls __init__;
  • then with calls __enter__;
  • then as stores the result of __enter__ into test
  • since __enter__ doesn't have a return value, test is None.

In second:

  • the object is created, which calls __init__;
  • then assigned to test;
  • then with calls __enter__;
  • but nothing is done with the result of __enter__;
  • so 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.

entonio
  • 2,143
  • 1
  • 17
  • 27
0

Explanation:

__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.

Solution:

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.

U13-Forward
  • 69,221
  • 14
  • 89
  • 114