Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 5 of 10

with open(...) from lesson 6.2 guarantees the file closes. That guarantee comes from the context manager protocol, and you can implement it on your own objects.

Any object with __enter__ and __exit__ works in a with block.

The protocol

class Greeter:
    def __enter__(self):
        print("entering")
        return "the value bound by as"

    def __exit__(self, exc_type, exc_value, traceback):
        print("exiting")

with Greeter() as value:
    print(value)
  • __enter__ runs at the start. Whatever it returns is bound by as.
  • __exit__ runs at the end — guaranteed, exception or not.

Guaranteed cleanup

That guarantee is the whole point:

class Resource:
    def __enter__(self):
        print("acquired")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("released")

try:
    with Resource():
        print("working")
        raise ValueError("something broke")
except ValueError as e:
    print(f"caught: {e}")

"released" prints even though the block raised. Same idea as finally in lesson 6.6, but reusable and named.

The __exit__ arguments

Those three parameters describe the exception, or are all None if the block finished cleanly:

class Watcher:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            print("clean exit")
        else:
            print(f"exited with {exc_type.__name__}: {exc_value}")

with Watcher():
    pass

try:
    with Watcher():
        raise ValueError("oops")
except ValueError:
    print("propagated")

Returning True from __exit__ suppresses the exception. Return None (or nothing) to let it propagate — which is almost always what you want. Silently swallowing errors is the trap from lesson 6.6 in a new costume.

class Suppressor:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        return True

with Suppressor():
    raise ValueError("this vanishes")

print("still running")

contextlib.contextmanager

The class is verbose for simple cases. The @contextmanager decorator turns a generator into a context manager — everything before yield is setup, everything after is teardown:

from contextlib import contextmanager

@contextmanager
def greeting():
    print("before")
    yield "hello"
    print("after")

with greeting() as value:
    print(value)

For guaranteed cleanup you need try/finally, or an exception skips the teardown:

from contextlib import contextmanager

@contextmanager
def resource():
    print("acquired")
    try:
        yield "handle"
    finally:
        print("released")

try:
    with resource() as r:
        print(f"using {r}")
        raise ValueError("broke")
except ValueError:
    print("caught")

A practical example

A timer, using the pattern for something you'd genuinely write:

from contextlib import contextmanager

@contextmanager
def tracked(label):
    """Report entry and exit around a block of work."""
    print(f"[{label}] start")
    try:
        yield
    finally:
        print(f"[{label}] done")

with tracked("import"):
    total = sum(range(100))
    print(f"computed {total}")

Swap the prints for time.perf_counter() and you have a profiler.

Try it yourself

Checkpoint

  • __enter__ returns the as value; __exit__ always runs.
  • __exit__ receives exception details, or three Nones on a clean exit.
  • Returning True from __exit__ swallows the exception — rarely what you want.
  • @contextmanager turns a generator into a context manager: setup, yield, teardown.
  • Wrap the yield in try/finally so cleanup survives exceptions.

Questions about this lesson

Log in to ask a question.