Engineering Notes
Home/Languages/Python/Context Managers
Languages · Python

Context Managers

with guarantees that some cleanup action runs when the block exits — normally, via return, or via an exception propagating out — without needing an explicit try/finally. open() returns a context manager: an object that knows how to set something up on entry and tear it down on exit.

💻 Languages● Advanced⏱ 5 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • The with Statement — Guaranteed Cleanup
  • The Protocol: __enter__ and __exit__
  • Suppressing Exceptions via __exit__'s Return Value
  • contextlib.contextmanager — Generator-Based Context Managers
  • contextlib.suppress — a Built-In for a Common Pattern

1The with Statement — Guaranteed Cleanup

python
with open("data.txt") as f:
    contents = f.read()
# f is automatically closed here, even if .read() raised an exception

with guarantees that some cleanup action runs when the block exits — normally, via return, or via an exception propagating out — without needing an explicit try/finally. open() returns a context manager: an object that knows how to set something up on entry and tear it down on exit.

2The Protocol: __enter__ and __exit__

python
class Resource:
    def __enter__(self):
        print("acquiring")
        return self   # becomes the `as` target

    def __exit__(self, exc_type, exc_value, traceback):
        print("releasing")
        return False   # False (or None) = do NOT suppress any exception

with Resource() as r:
    print("using", r)

Output:

text
acquiring
using <Resource object ...>
releasing

with EXPR as name: desugars, roughly, to calling EXPR.__enter__() and binding its return value to name, running the block, then always calling EXPR.__exit__(exc_type, exc_value, traceback) on the way out — whether the block finished normally (all three arguments None) or an exception is propagating through it (all three populated).

3Suppressing Exceptions via __exit__'s Return Value

python
class SwallowValueError:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is ValueError:
            print(f"suppressed: {exc_value}")
            return True   # True = suppress this exception, block exits normally
        return False        # anything else propagates as usual

with SwallowValueError():
    raise ValueError("boom")   # caught and suppressed by __exit__

print("still running")           # this DOES run — the exception never escaped the with block

If __exit__ returns a truthy value, the exception that was propagating through the with block is suppressed entirely — code after the with block continues as if nothing happened. Returning False/None (the default if you don't explicitly return anything) lets the exception propagate normally after cleanup runs. This is deliberate, explicit opt-in behavior — accidentally suppressing exceptions by forgetting to return False is a real footgun (see Common Pitfalls).

4contextlib.contextmanager — Generator-Based Context Managers

python
from contextlib import contextmanager

@contextmanager
def resource():
    print("acquiring")
    try:
        yield "the resource"       # this becomes the `as` target
    finally:
        print("releasing")          # ALWAYS runs, exception or not

with resource() as r:
    print("using", r)

Writing a full class with __enter__/__exit__ is verbose for simple cases. @contextmanager turns a generator function into a context manager: everything before yield is the "enter" logic, the yielded value is the as target, and everything after yield (typically in a finally block) is the "exit" logic. An exception raised inside the with block is re-raised at the yield point inside the generator, which is exactly why the cleanup code needs to sit in a finally (or a try/except around the yield) to run reliably.

5contextlib.suppress — a Built-In for a Common Pattern

python
from contextlib import suppress

with suppress(FileNotFoundError):
    import os
    os.remove("maybe_missing.txt")
# no exception, no error, whether the file existed or not

suppress(*exception_types) is a ready-made context manager equivalent to a try/except: pass for exactly the listed exception types — useful for "best-effort cleanup" code where a specific, expected failure should be silently ignored, without writing a full try/except for a one-liner.

6Multiple Context Managers in One with

python
with open("in.txt") as src, open("out.txt", "w") as dst:
    dst.write(src.read())

Comma-separating context managers in one with statement enters them left to right and exits them in reverse order (right to left) — equivalent to nesting them, but flatter and more readable. If entering the second one raises, the first one's __exit__ still runs correctly (Python handles this internally, equivalent to nested with blocks under the hood).

Under the HoodWhat with Actually Desugars To▾
python
with EXPR as name:
    BODY

is equivalent to:

python
mgr = EXPR
aexit = type(mgr).__exit__   # looked up on the TYPE, not the instance
aenter = type(mgr).__enter__
value = aenter(mgr)
exc = True
try:
    try:
        name = value
        BODY
    except:
        exc = False
        if not aexit(mgr, *sys.exc_info()):
            raise
finally:
    if exc:
        aexit(mgr, None, None, None)

The key detail most people miss: __enter__/__exit__ are looked up on the object's type, not the instance — the same special-method lookup rule that applies to operator overloading (__add__, __len__, etc.) applies here too, meaning you can't monkey-patch __exit__ onto a single instance and have with see it. This is also exactly why @contextmanager-decorated generator functions work: the decorator returns a class (_GeneratorContextManager) whose __enter__/__exit__ methods drive the generator via next()/.throw(), translating the class-based protocol into the generator's yield-based one underneath.

with calls __enter__, runs the body, then always calls __exit__; on an exception, a truthy return from __exit__ swallows it and a falsy one re-raises it
with calls __enter__, runs the body, then always calls __exit__; on an exception, a truthy return from __exit__ swallows it and a falsy one re-raises it

⚠️ Common Pitfalls

  1. Forgetting to return False/None from a custom __exit__ — if the last statement happens to evaluate truthy (e.g. return exc_type is None when there was no exception evaluates to True), it silently suppresses real exceptions raised inside the block; be explicit about the return value.
  2. Writing @contextmanager cleanup logic without try/finally around the yield — if the block raises, that exception is re-raised at the yield point; without finally, an exception skips the cleanup code entirely, defeating the purpose of using a context manager at all.
  3. Reusing a single context manager instance across multiple with blocks concurrently — most context managers (including file objects) are not designed to be entered twice at once; each with should generally get its own instance unless the class documents reentrancy support.
  4. Assuming contextlib.suppress catches subclasses you didn't list — it uses normal exception matching, so suppress(ValueError) does not catch a TypeError, even in code where you expected "any input-related error."
  5. Using a bare try/finally instead of with for resources that already provide a context manager — reinvents cleanup logic that's already correctly handled (including the exception-info threading through __exit__), and is easy to get subtly wrong (e.g. forgetting to close on every exception path).

📋 Quick Reference

python
with EXPR as name:                          # __enter__() -> name, guarantees __exit__() on exit
    ...

class C:
    def __enter__(self): return self          # setup, returns the `as` target
    def __exit__(self, exc_type, exc_val, tb):  # ALWAYS called; return True to suppress exception
        return False

from contextlib import contextmanager
@contextmanager
def cm():
    setup()
    try:
        yield value          # the `as` target
    finally:
        teardown()             # always runs

from contextlib import suppress
with suppress(SomeError): ...     # try/except SomeError: pass, as a context manager

with a() as x, b() as y: ...        # enters left-to-right, exits right-to-left

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1What is a context manager?core▾

An object used with with that sets up and tears down a resource reliably: __enter__ runs on entry, __exit__ on exit even if an exception occurs. Used for files, locks, transactions, connections.

Follow-ups interviewers ask
  • Can __exit__ suppress exceptions? Yes, by returning True.
Q2How do you write one with contextlib?core▾

@contextmanager def timer(): start = perf_counter(); try: yield; finally: log(perf_counter() - start). Code before yield is setup, after is teardown.

Follow-ups interviewers ask
  • Why try/finally around yield? Ensures cleanup on exceptions.
Q3What does ExitStack do?core▾

Manages a dynamic number of context managers, entering them in a loop and exiting all in reverse order, even on errors.

Follow-ups interviewers ask
  • Use case? Opening a variable list of files.
Q4What are async context managers?senior▾

Objects with __aenter__/__aexit__ used with async with, for async resources like DB sessions or HTTP clients; @asynccontextmanager creates them.

Follow-ups interviewers ask
  • Example? async with httpx.AsyncClient() as client:.
Q5Give practical examples in backend code.core▾

Database transactions (with session.begin():), locks (with lock:), temporary files/directories, patching in tests (with mock.patch(...)), and suppressing specific errors (contextlib.suppress).

Follow-ups interviewers ask
  • Why transactions as context managers? Automatic commit/rollback.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.