How does a context manager work, and how does the `with` statement use `__enter__`/`__exit__`?

6 minintermediatecontext-managerswith-statement

Quick Answer

A context manager implements `__enter__` (run when entering the `with` block, its return value bound to the `as` variable) and `__exit__` (run when leaving the block, **even if an exception occurred**, receiving the exception info and able to suppress it by returning `True`). This guarantees cleanup (closing a file, releasing a lock, committing/rolling back a transaction) happens regardless of how the block exits.

Detailed Answer

What with desugars to

with open("file.txt") as f:
    data = f.read()

# roughly equivalent to:
f = open("file.txt").__enter__()
try:
    data = f.read()
finally:
    open("file.txt").__exit__(None, None, None)   # (simplified)

__enter__() runs first, and its return value is bound to f (the as target). __exit__() is guaranteed to run when the block ends, whether it ends normally or via an exception — this is the core guarantee with provides over a manual try/finally written by hand every time.

Writing your own context manager (class-based)

class Timer:
    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        import time
        self.elapsed = time.perf_counter() - self.start
        print(f"Elapsed: {self.elapsed:.3f}s")
        return False    # False (or None) -- don't suppress any exception

with Timer() as t:
    do_expensive_work()
# prints elapsed time whether do_expensive_work() succeeded or raised

__exit__ receives (exc_type, exc_value, traceback) — all None if the block exited normally, or the actual exception info if one was raised. Returning True from __exit__ suppresses the exception (swallows it, as if it never happened); returning False/None lets it propagate normally after cleanup runs.

A common real-world pattern: guaranteed release

class DatabaseTransaction:
    def __enter__(self):
        self.conn.begin()
        return self.conn

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            self.conn.commit()
        else:
            self.conn.rollback()   # roll back on ANY exception
        return False               # still propagate the exception after rollback

This is the canonical use of __exit__'s exception parameters: commit on success, roll back on failure, and let the caller still see the original exception (since returning False doesn't hide it) — the transaction is never left half-applied regardless of how the block exits.

Multiple context managers in one with

with open("in.txt") as fin, open("out.txt", "w") as fout:
    fout.write(fin.read())

Equivalent to nested with blocks — each context manager's __exit__ is guaranteed to run (in reverse order of entry) even if a later one's __enter__ or the block body fails.

Interview-ready summary: with guarantees __exit__ runs on the way out of the block, exception or not — __enter__ sets up a resource and __exit__ tears it down, optionally inspecting/suppressing an exception. This is Python's structured alternative to manually writing try/finally around every resource-acquiring operation.