Home Tutorials Resource Management

Resource Management

Context Managers in Python: with Statements and the Protocol Behind Them

Pyford Notes July 1, 2026 8 min read
Key points
  • The with statement guarantees that cleanup code runs even if an exception occurs.
  • Any object implementing __enter__ and __exit__ is a context manager.
  • __exit__ receives the exception info and can suppress it by returning a truthy value.
  • contextlib.contextmanager lets you write a manager as a generator function.

The setup and teardown problem

Many operations follow a three-phase pattern: acquire a resource, use it, release it. Files, database connections, locks, and network sockets all work this way. The release step must happen even if the usage step raises an exception, or you leak the resource.

Without a context manager, you need try/finally to guarantee cleanup:

lock = threading.Lock()
lock.acquire()
try:
    shared_data.append(item)
finally:
    lock.release()   # must happen no matter what

This is correct but verbose. When every operation that touches a resource needs this boilerplate, the code becomes cluttered and the teardown logic scattered. Context managers consolidate the pattern.

The with statement

The with statement wraps a block of code with an object that handles setup and teardown:

with open("data.txt", encoding="utf-8") as fh:
    content = fh.read()
# fh.close() was called here, exception or not

The optional as clause binds whatever __enter__ returns to a name. For a file, __enter__ returns the file object itself, so fh is both the context manager and the resource you work with.

You can enter multiple context managers in a single statement, which is cleaner than nesting:

with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

The __enter__ and __exit__ protocol

Under the hood, with expr as name is equivalent to:

manager = expr
name = manager.__enter__()
try:
    # body of the with block
finally:
    manager.__exit__(exc_type, exc_val, exc_tb)

__enter__ is called on entry. Its return value is bound to the as target (if present).

__exit__ is always called on exit and receives three arguments: the exception type, value, and traceback (all None when no exception occurred). If __exit__ returns a truthy value, the exception is suppressed and execution continues after the with block. Returning None or False lets the exception propagate.

Writing a class-based context manager

Implement both dunder methods on a class to make it a context manager:

import time

class Timer:
    def __enter__(self):
        self._start = time.perf_counter()
        return self          # bound to "as" target

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.elapsed = time.perf_counter() - self._start
        return False         # do not suppress exceptions

with Timer() as t:
    expensive_operation()
print(f"Elapsed: {t.elapsed:.3f}s")

A context manager that suppresses a specific exception type:

class SuppressOSError:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        return exc_type is OSError   # True suppresses the exception

The contextlib shortcut

For simple managers that do not need to suppress exceptions, contextlib.contextmanager turns a generator function into a context manager. The code before yield is the setup; the code after is the teardown:

from contextlib import contextmanager
import tempfile, os

@contextmanager
def temp_directory():
    path = tempfile.mkdtemp()
    try:
        yield path                # body of the with block runs here
    finally:
        import shutil
        shutil.rmtree(path)       # always clean up

with temp_directory() as d:
    with open(os.path.join(d, "scratch.txt"), "w") as fh:
        fh.write("temporary data")
# directory is deleted here

contextlib also ships ready-made managers for common patterns: suppress(ExcType) silently ignores a specific exception; redirect_stdout(stream) redirects stdout within the block; ExitStack manages a dynamic number of context managers assembled at runtime.

Frequently asked questions

Does with guarantee teardown if I call sys.exit()?

sys.exit() raises SystemExit, which is an exception. __exit__ is called and runs teardown code unless the interpreter process itself is killed by a signal or power loss.

Can I reuse a context manager?

Depends on the implementation. A file object can only be used once (reopening it requires creating a new one). A threading.Lock can be entered and exited repeatedly. Check the documentation for the specific object.

When should I write my own vs use an existing one?

Write your own when you have a repeating setup/teardown pattern in your own code. If the pattern exists in the standard library or a well-maintained package (tempfile.TemporaryDirectory, unittest.mock.patch), prefer the existing implementation.