Python Docs

Context Managers

Context managers guarantee proper acquisition and release of resources. They are used with the with statement and handle setup/teardown logic safely even when exceptions occur.

Basic Usage

The most common example is working with files. The file will always be closed when the block exits.

# File automatically closes after block
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("hello")
  • If an exception happens inside the block, the file is still safely closed.
  • The object returned by open(...) is bound to f.

Creating a Context Manager via Class

A custom context manager class implements __enter__ and __exit__.

class Managed:
    def __enter__(self):
        print("acquire")
        return self

    def __exit__(self, exc_type, exc, tb):
        print("release")
        # Return True to suppress exception, False to propagate
        return False

with Managed() as m:
    print("work")
  • __enter__ runs at the start; its return value becomes as m.
  • __exit__(exc_type, exc, tb) always runs, even if an exception happens.
  • Returning True swallows the exception (use carefully).

Using contextlib.contextmanager

You can write context managers using a generator function with a yield instead of a class.

from contextlib import contextmanager

@contextmanager
def db_session(engine):
    conn = engine.connect()
    try:
        yield conn          # code inside "with" block runs here
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()
with db_session(engine) as conn:
    conn.execute("INSERT INTO logs(message) VALUES ('ok')")
  • Code before yield is setup (acquire).
  • Code after yield runs on exit (teardown).
  • Exceptions inside the with are caught in the except block.

Helpful Tools in contextlib

from contextlib import suppress, ExitStack, closing
import socket

# Suppress a specific exception
with suppress(FileNotFoundError):
    open("missing.txt").read()

# Manage many contexts dynamically
files = ["a.txt", "b.txt"]
with ExitStack() as stack:
    handles = [stack.enter_context(open(p, "w")) for p in files]
    handles[0].write("hello")

# Ensure close() is called on arbitrary objects
with closing(socket.socket()) as s:
    s.bind(("127.0.0.1", 0))
  • suppress is useful for ignoring expected errors (e.g., missing files).
  • ExitStack is great when you don’t know in advance how many with contexts you'll need.
  • closing adapts any object with .close() into a context manager.

Real-World Examples

import threading
from contextlib import contextmanager

lock = threading.Lock()

@contextmanager
def locked():
    lock.acquire()
    try:
        yield
    finally:
        lock.release()

with locked():
    # critical section
    print("Only one thread at a time")
import time
from contextlib import contextmanager

@contextmanager
def timed(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        duration = time.perf_counter() - start
        print(f"{label} took {duration:.4f}s")

with timed("heavy_task"):
    sum(range(10_000_000))

Best Practices

  • Prefer with for files, locks, DB sessions, network sockets, temporary directories, etc.
  • Make __exit__ / teardown code idempotent and exception-safe.
  • Be careful when suppressing exceptions; document clearly what is being suppressed and why.
  • If your setup/cleanup logic repeats everywhere → wrap it in a context manager.