Command Palette

Search for a command to run...

Hectal
PHASE 9Intermediate ~25 min· topic 8 of 9

Topic 9.8

Writing Context Managers with contextlib

In one line

contextlib lets you write your own with blocks using a generator and @contextmanager, and gives tools for common jobs: suppress to ignore an error, ExitStack to manage a changing number of resources, closing, redirect_stdout and nullcontext.

Think of it like this

Borrowing a library book. You check it out (setup), read it (the work), and the book must go back, even if you are interrupted or lose interest halfway (cleanup). A context manager guarantees the "go back" part. The with statement is the library desk: whatever happens inside, the cleanup runs.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Context manager
An object that does setup when a with block starts and cleanup when it ends.
with statement
Runs a block between the context manager's setup and cleanup.
__enter__ / __exit__
The two special methods behind with: one at the start, one at the end.
@contextmanager
A decorator that turns a one-yield generator function into a context manager.
try / finally
Code in finally runs whatever happens in try, even after an exception (Topic 7.1).
ExitStack
A container that can hold any number of context managers and clean them up in reverse order.
suppress
A context manager that quietly ignores the named exception types.
Buffer (StringIO)
An in-memory text object that behaves like a file.

Step by step

01The with statement, spelled out

with A() as x: body does roughly this: call A().__enter__() and give the result to x; run body inside a try; whatever happens, call __exit__. If __exit__ returns a true value, an exception from the body is swallowed; otherwise it continues up.

The with statement, spelled outdiagram
Rendering diagram…

02A generator-based context manager

announce prints a start line, yields a value, and prints an end line in finally. Because of finally, the end line prints even when the body raises an error.

main.pywhole filepython
from contextlib import contextmanager

@contextmanager
def announce(name):
    print(f"start {name}")
    try:
        yield name.upper()
    finally:
        print(f"end {name}")

with announce("job") as label:
    print("working on", label)

03Why try/finally matters

If the body raises, the exception is thrown into the generator **at the yield**. Without try/finally, it ends the generator right there and the cleanup code is skipped. The error then continues upward. The break-it example runs this without finally so you can see the "end" line missing.

04suppress and nullcontext

with suppress(FileNotFoundError): open("missing.txt") replaces four lines of try/except/pass. Only the listed errors are ignored; other errors still raise. nullcontext() is a with that does nothing, useful when a resource is optional.

05ExitStack for a dynamic number of resources

You cannot write with for a list of files whose length you do not know. ExitStack solves it: enter each manager into the stack, and they are all closed together at the end, last opened first closed. stack.callback(func, *args) registers an arbitrary cleanup call.

06closing and redirect_stdout

closing(obj) calls obj.close() on exit for old-style objects. redirect_stdout(buffer) temporarily replaces sys.stdout, so every print inside the block goes into the buffer. Afterwards buffer.getvalue() has the captured text. The original stdout is restored even after an error.

07Class form versus generator form

Use @contextmanager for simple setup and cleanup. Write a class with __enter__ and __exit__ when the context manager needs to be reusable several times, needs extra methods, or must decide itself whether to swallow an exception. Since Python 3.2, a context manager made with @contextmanager can also be used as a decorator on functions.

Try it yourself

  1. 1

    Time a block without printing the time

    Write a @contextmanager named timed(label) that prints start label, yields, and prints done label in finally. Use it around a loop. In real code you would record time.perf_counter() before and after and log the difference; here we print fixed text so the output never changes.

  2. 2

    Open three files at once

    Extend the ExitStack example to write and read three files. Check that all(f.closed ...) is still True.

Code & diagrams

contextmanager, suppress and nullcontext New tab

Remove the try/finally in announce and run again. The 'end risky' line disappears when the body fails.

Sign in to run this example in your browser.

Expected output

start job
working on JOB
end job
start risky
end risky
caught: boom
suppress swallowed the error
processing
start locked
processing
end locked
ExitStack, closing and redirect_stdout New tab

Add a third file name to the list. ExitStack closes however many you open.

Sign in to run this example in your browser.

Expected output

['one', 'two']
cleanup callback ran
all closed: True
using connection
connection closed
captured: 'this goes into the buffer\n'
A realistic context manager: commit or roll back New tab

Databases and payment code use exactly this shape: change things inside the block, keep the changes if it finishes, undo them if it fails. The except branch undoes the change and then re-raises, so the caller still sees the error.

Sign in to run this example in your browser.

Expected output

committed
after success: {'balance': 70}
rolled back because: insufficient funds
after failure: {'balance': 70}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Cleanup without try/finally

Write the cleanup after the yield but without a try/finally.

main.pywhole filepython
from contextlib import contextmanager

@contextmanager
def announce(name):
    print(f"start {name}")
    yield name
    print(f"end {name}")

try:
    with announce("risky"):
        raise ValueError("boom")
except ValueError:
    print("caught")
terminal
$ python main.py
── what you'll see ──
start risky
caught

Myth vs fact

Myth

with only works for files.

Fact

It works for anything with __enter__ and __exit__: locks, database transactions, temporary directories, network connections, and your own context managers.

Myth

Code after the yield in a @contextmanager always runs.

Fact

Only if you put it in finally. If the body raises and there is no try/finally, it is skipped.

Myth

suppress hides all errors.

Fact

It ignores only the exception types you name. Others propagate normally.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    @contextmanager is built on the generator methods from Topic 9.2: on entry it calls next() on the generator; on exit with no error it calls next() again and expects StopIteration; on exit with an error it calls gen.throw(error) so the exception appears at the yield. If the generator yields a second time, you get RuntimeError: generator didn't stop. The object that @contextmanager returns is single-use: entering the same one twice fails (call the function again for a fresh one). Calling it with @mgr() as a decorator, however, builds a fresh one for each call. Also: if __enter__ itself raises, __exit__ is not called, so put acquisition that can fail before the try, and cleanup only for what was really acquired.

  • ▸

    If your cleanup code catches the exception and does not re-raise it, @contextmanager swallows it. To swallow deliberately, catch it around the yield and do not raise; otherwise let it propagate (or use finally, which does not swallow).

  • ▸

    Python 3.10 added parenthesised with items, so several managers can be written across lines in one statement. For a runtime-chosen number, ExitStack is still the tool, and AsyncExitStack with @asynccontextmanager does the same for async with (Phase 12). Python 3.11 added contextlib.chdir(path) for a temporary change of working directory, and aclosing is the async version of closing. With several managers in one with, they exit in reverse order, and if two exits raise, the later error is chained to the earlier.

  • ▸

    In AI and data services, context managers guard transactions (commit on success, roll back on error), temporary configuration changes (restore the old value in finally), and timing or tracing spans around each call to a model.

Remember this

  1. 1

    You met context managers as a user in Topic 7.4: with open(path) as f:. The with statement calls __enter__ at the start, runs the block, and calls __exit__ at the end, even if an exception happened. Writing a class with those two methods works, but contextlib.contextmanager does it in a few lines with a generator.

  2. 2

    Recall from Topic 9.2: a generator pauses at each yield and resumes later. @contextmanager turns a generator function with **exactly one yield** into a context manager. Code before the yield is the setup (__enter__). The value yielded is what as receives. Code after the yield is the cleanup (__exit__). So a generator pauses at the yield while the with body runs, then continues afterwards.

  3. 3

    Put the yield inside try: and the cleanup in finally:. Without try/finally, an exception in the with body is raised at the yield, the generator ends early, and the cleanup lines after the yield never run. This is the most important rule of this topic, and the break-it example shows it.

  4. 4

    suppress(ErrorType) ignores the named errors inside the block: with suppress(FileNotFoundError): os.remove(path). It reads better than an empty try/except. nullcontext() does nothing: it stands in when a context manager is optional (with lock or nullcontext():). closing(obj) calls obj.close() at the end for objects that have a close method but are not context managers themselves.

  5. 5

    ExitStack manages a changing number of context managers or cleanup steps. You open files in a loop with stack.enter_context(open(name)), register extra cleanup with stack.callback(func, ...), and when the with ExitStack() block ends, everything is closed in the reverse order, even if one of them fails. Use it when you cannot write a fixed number of with items.

  6. 6

    redirect_stdout(target) sends print output into a buffer such as io.StringIO(), which is useful in tests and for capturing what a noisy function prints. In AI and data code, custom context managers are common for timing a block, holding a database transaction, temporarily changing a setting, or opening and closing a connection to a model service or vector database.

Explain it without notes

01

What does the with statement guarantee, and how do __enter__ and __exit__ fit in?

02

How does @contextmanager turn a generator into a context manager, and what must you do around the yield?

03

When would you use ExitStack?

04

What do suppress, nullcontext and closing each do?

Practice

01

Write a @contextmanager named banner(title) that prints === title === before the block and === end === after it (in finally). Use it around one print.

02

Use suppress so that deleting a missing key from a dictionary does not raise: del data["zzz"], then print data.

03

Write a @contextmanager named temporary_value(box, key, value) that sets box[key] = value for the block and restores the old value afterwards. Show the dictionary before, inside and after.

Trade-offs

  • ↔

    Short generator form versus a full class. @contextmanager is short and clear for one-off setup/cleanup, but a class is better when the manager is reused, has extra methods or needs fine control over exceptions.

  • ↔

    Guaranteed cleanup versus hidden control flow. with makes cleanup certain, but a context manager that swallows exceptions can hide bugs. Swallow errors only on purpose, and name the manager so that is obvious (suppress).

Done when you can

  • Done when you can explain what __enter__ and __exit__ do in a with block.

  • Done when you can write a context manager with @contextmanager using try/finally.

  • Done when you can explain why the cleanup is skipped without finally.

  • Done when you can use suppress, nullcontext and closing for their jobs.

  • Done when you can use ExitStack to open a list of files.