Python · Errors & Exceptions

Python Context Managers and the with Statement Explained

4 min read Updated 2026-06-19

Practice Context Managers & with interview questions

Python context managers, explained

with open(...) as f: is the first context manager every Python developer meets, but the pattern goes far beyond files. A context manager guarantees that setup and teardown happen reliably, even when an exception is raised — which is why interviewers use it to probe your understanding of resource management and the protocol underneath. This guide covers both ways to build one and the exception-handling subtleties.

What the with statement does

A with block calls an object's __enter__ on the way in and its __exit__ on the way out — guaranteed, whether the block finishes normally, returns, or raises. It's structured cleanup without a manual try/finally.

with open("data.txt") as f:     # __enter__ runs, returns the file
    data = f.read()
# __exit__ runs here no matter what — the file is closed

The value after as is whatever __enter__ returns. The win over try/finally is that the cleanup logic lives with the resource, not scattered around every use site.

The protocol: enter and exit

Any object implementing __enter__ and __exit__ is a context manager. __enter__ does setup and returns the value bound by as; __exit__(exc_type, exc_val, exc_tb) does teardown and receives details of any exception that occurred.

class Resource:
    def __enter__(self):
        print("acquire")
        return self                # bound to the `as` variable
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("release")           # always runs
        return False               # don't suppress exceptions

with Resource() as r:
    print("using", r)
# acquire / using / release

If the block raised, the three exc_* arguments describe it; if it exited cleanly, they're all None.

Handling exceptions in exit

__exit__ is where the resource-management subtlety lives. Its return value decides whether the exception propagates: return a falsy value (the default) and the exception re-raises after cleanup; return True and you suppress it.

class Suppress:
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is ValueError:
            print("swallowing", exc_val)
            return True            # suppress ValueError only
        return False               # let everything else propagate

with Suppress():
    raise ValueError("boom")       # swallowed; program continues

The trap: an accidental return True silently hides every error. Default to returning False (or nothing) unless suppression is genuinely intended.

The easy way: contextlib.contextmanager

For simple cases, writing a whole class is overkill. The @contextmanager decorator turns a generator into a context manager: everything before yield is setup, the yielded value is the as target, and everything after yield is teardown.

from contextlib import contextmanager

@contextmanager
def timer(label):
    import time
    start = time.perf_counter()           # setup
    try:
        yield                             # the with-block runs here
    finally:
        print(f"{label}: {time.perf_counter() - start:.4f}s")   # teardown

with timer("query"):
    run_query()

The try/finally around yield is what makes teardown run even on exception — the exception is re-raised at the yield inside the generator, so finally catches it.

Managing multiple resources

You can open several context managers in one with, and they're entered left-to-right and exited in reverse — so dependent resources tear down in the right order.

with open("in.txt") as src, open("out.txt", "w") as dst:
    dst.write(src.read())
# dst closes first, then src

For a dynamic or unknown number of resources, contextlib.ExitStack lets you enter context managers in a loop and guarantees they all close.

Where context managers earn their keep

Anywhere there's a paired acquire/release: files (close), locks (with lock: acquire/release), database connections/transactions (commit or rollback), temporary directories, and patching in tests.

import threading
lock = threading.Lock()

with lock:                 # __enter__ acquires, __exit__ releases — even on error
    update_shared_state()

The guarantee that release happens even when the body raises is exactly why locks and transactions use this pattern.

Recap

A context manager pairs __enter__ (setup) with __exit__ (teardown), and the with statement guarantees teardown runs even on exceptions. __exit__'s return value controls propagation — falsy re-raises (the safe default), True suppresses. For simple cases, @contextlib.contextmanager turns a generator into a context manager with a try/finally around yield. Multiple managers exit in reverse order, and ExitStack handles dynamic sets. Reach for with whenever a resource must be released no matter what — it's cleaner and safer than hand-rolled try/finally.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.