Wednesday, September 21, 2022

Python Context Manager


By Llewellyn Falco & Matt Plavcan

I've been doing a lot python lately and context Manager has thrown me for a bit of a loop. Here's what happened

Context

First, A ContextManager is what python uses for the with statement. It is the python equivalent of:

  • java's try with resources
  • c#'s using

It allows you to do the following:

@contextmanager
def printer():
    print("enter")
    yield
    print("exit")

if __name__ == '__main__':
    with printer():
        print("middle")

Which prints

enter
middle
exit

Problem 1) Yield fails with exceptions

Let's say we have

@contextmanager
def printer():
    print("enter")
    yield
    print("exit")

if __name__ == '__main__':
    with printer():
        raise Exception("middle")

I would expect it to close the context and then pass on the Exception. Like such:

enter
Exception: middle
exit

But it doesn't. Instead, I get:

enter
Exception: middle

There is no exit printed.

Solution 1) Use ContextManager class

I need to expand the Printer to a full class. ContextManager is just an interface that assumes you have the __enter__ and __exit__ methods defined on a class.

For example:

class Printer():
    def __enter__(self):
        print("enter")

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("exit")

if __name__ == '__main__':
    with Printer():
        print("Middle")
        raise Exception("from middle")

Now I get the expected behavoir:

enter
Middle
exit

Problem 2) ContextManager is None?

But what if I want to reference the ContextManager? Python allows you to assign the object to a variable with the as variable_name syntax.

Let's try to print the ContextManager reference:

    class Printer():
        def __enter__(self):
            print("enter")

        def __exit__(self, exc_type, exc_val, exc_tb):
            print("exit")

        def __str__(self):
            return "<Printer>"

    if __name__ == '__main__':
        with Printer() as p:
            print(f"p = {p}")

Surprisingly this produces:

enter
p = None
exit

What is going on? The __enter__ and __exit__ work, but the value is somehow None? How can both be true?

Solution 2) How assignment from ContextManager works.

You would think that:

with Printer() as p

is the same as

p = Printer()
with p:

but it's not. It's actually

_p = Printer()
p = p.__enter__()

Here's the solution

class Printer():
    def __enter__(self):
        print("enter")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("exit")

    def __str__(self):
        return "<Printer>"

if __name__ == '__main__':
    with Printer() as p:
        print(f"p = {p}")

This produces the expected:

enter
p = <Printer>
exit

What's different? The __enter__ now returns self. Before it had no return, so there was an implicit return None that all python methods have as a minimum.