Phase 4 · Intermediate PythonModule 25~30 min read

Context Managers

Manage resources safely with the with statement and the context-manager protocol.

What you'll learn

You've used with open(...) to guarantee files close. That's a context manager — a protocol for setup-and-cleanup that always runs, even on error. Now you'll write your own, for timing, transactions, temporary state, or any resource that must be released.

By the end you'll be able to:

  • Explain what the with statement guarantees
  • Write a class-based context manager (__enter__/__exit__)
  • Write one concisely with @contextmanager
  • Use multiple managers together

The with statement, revisited

A context manager wraps a block with guaranteed enter and exit steps. The exit runs no matter how the block ends — normal completion, a return, or an exception. That's why with open(...) can promise the file is always closed. Anything that must be cleaned up — files, locks, database connections, network sockets — is a candidate.

Class-based managers

Implement two dunder methods: __enter__ (setup; its return value is bound to the as variable) and __exit__ (teardown; it receives any exception info). Here's a timer that reports how long its block took:

timer.py
class Timer:
    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self                       # value bound to 'as'

    def __exit__(self, exc_type, exc_val, tb):
        import time
        self.elapsed = time.perf_counter() - self.start
        print(f"Took {self.elapsed:.4f}s")
        # return False -> don't suppress exceptions

with Timer() as t:
    total = sum(range(1_000_000))
print("Sum:", total)

Note

__exit__ receives the exception type, value, and traceback (or three Nones if the block succeeded). Return True from it to suppress the exception; return False (or nothing) to let it propagate normally — usually what you want.

@contextmanager

Writing a class is often overkill. The contextlib.contextmanager decorator turns a generator into a context manager: everything before yield is setup, everything after is teardown, and the yielded value goes to as:

tag.py
from contextlib import contextmanager

@contextmanager
def tag(name):
    print(f"<{name}>")        # setup (before yield)
    yield                     # the 'with' body runs here
    print(f"</{name}>")       # teardown (after yield)

with tag("p"):
    print("Hello")

Wrap the yield in try/finally and your cleanup runs even when the block raises — exactly the guarantee that makes context managers so valuable:

safe.py
from contextlib import contextmanager

@contextmanager
def managed_resource():
    print("acquire")
    try:
        yield "resource"
    finally:
        print("release")     # runs even if the body raises

try:
    with managed_resource() as r:
        print("using", r)
        raise ValueError("boom")
except ValueError:
    print("handled error")

Key idea

This is the whole point: cleanup is guaranteed. Whether the block finishes normally or blows up, the teardown after yield (inside finally) still runs. No leaked files, no dangling locks, no forgotten cleanup.

Multiple managers

You can open several context managers in one with, separated by commas — handy for copying between two files. They enter left-to-right and exit right-to-left:

multiple.py
with open("in.txt") as src, open("out.txt", "w") as dst:
    dst.write(src.read())
# both files are closed here, in reverse order

Recap & quick check

Key takeaways

  • A context manager guarantees setup and cleanup around a block — cleanup runs even on error.
  • Class-based: __enter__ (setup; return value goes to 'as') and __exit__ (teardown; gets exception info).
  • __exit__ returning True suppresses the exception; False/None lets it propagate.
  • @contextlib.contextmanager turns a generator into a manager: before yield = setup, after = teardown.
  • Wrap yield in try/finally for guaranteed cleanup; combine multiple managers with commas in one with.

Quick check

1. What does a context manager guarantee?

2. Which two methods implement a class-based context manager?

3. In @contextmanager, what does the code before yield represent?

4. How do you guarantee cleanup runs even if the with-body raises?

5. What does returning True from __exit__ do?

That completes Phase 4 — Intermediate Python! 🎉 Next we enter Phase 5 — Advanced Python with Module 26 — Type Hints & Static Typing.