Strange behavior with contextmanager

Question:

Take a look at the following example:

from contextlib import AbstractContextManager, contextmanager


class MyClass(AbstractContextManager):
    _values = {}

    @contextmanager
    def using(self, name, value):
        print(f'Allocating {name} = {value}')
        self._values[name] = value
        try:
            yield
        finally:
            print(f'Releasing {name}')
            del self._values[name]

    def __enter__(self):
        return self.using('FOO', 42).__enter__()

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass


with MyClass():
    print('Doing work...')

I would expect the above code to print the following:

Allocating FOO = 42
Doing work...
Releasing FOO

Instead, this is what is being printed:

Allocating FOO = 42
Releasing FOO
Doing work...

Why is FOO getting released eagerly?

Asked By: Matias Cicero

||

Answers:

You’re creating two context managers here. Only one of those context managers is actually implemented correctly.

Your using context manager is fine, but you’ve also implemented the context manager protocol on MyClass itself, and the implementation on MyClass is broken. MyClass.__enter__ creates a using context manager, enters it, returns what that context manager’s __enter__ returns, and then throws the using context manager away.

You don’t exit the using context manager when MyClass() is exited. You never exit it at all! You throw the using context manager away. It gets reclaimed, and when it does, the generator gets close called automatically, as part of normal generator cleanup. That throws a GeneratorExit exception into the generator, triggering the finally block.

Python doesn’t promise when this cleanup will happen (or indeed, if it will happen at all), but in practice, CPython’s reference counting mechanism triggers the cleanup as soon as the using context manager is no longer reachable.


Aside from that, if _values is supposed to be an instance variable, it should be set as self._values = {} inside an __init__ method. Right now, it’s a class variable.

Answered By: user2357112