Managing Resources Automatically

A resource is anything a program holds that must be released: an open file, a network socket, a lock, a database cursor, a subprocess. Forgetting to release even one of them is a classic source of bugs — file handles leak, locks deadlock, connections pile up. Python's answer is the context manager: an object that knows how to clean up after itself and cooperates with the with statement.

The with statement takes a context manager, calls its __enter__ method on entry, and guarantees that its __exit__ method runs on the way out — even if an exception fires. Writing with open(path) as f: ... is the idiomatic way to read a file in Python precisely because it can never forget to close. The pattern extends to locks (threading.Lock), transactions, temporary directories, timers — any scope with setup and cleanup.

Writing your own context manager is two-line simple thanks to contextlib.contextmanager. Decorate a generator that yields once; the code before the yield is the setup, the code after is the cleanup. Exceptions from the with block raise at the yield so you can handle or propagate them. This is by far the most common way real projects write context managers.

For grouping many resources, contextlib.ExitStack lets you register cleanups dynamically and unwinds them in LIFO order. It is the tool when the number of resources is only known at runtime — opening N files from a list, for example. Between with, contextmanager, and ExitStack, cleanup code usually disappears from your programs entirely.

The with statement and built-in managers

with open(path) as f:, with lock:, with connection: are the everyday forms. Use nested or comma-separated forms for multiple managers: with open(a) as f, open(b) as g:.

When a function returns a resource, return the context manager, not the resource. Callers always get the cleanup guarantee that way.

Writing your own

from contextlib import contextmanager; @contextmanager def timer(name): start = time.perf_counter(); try: yield finally: print(...). The try/finally is the cleanup; the yield is the point where user code runs.

For a class-based manager, define __enter__ and __exit__(self, exc_type, exc, tb). Return True from __exit__ to suppress an exception (rarely right); False or None to let it propagate.

Context manager tools.

ToolPurpose
with mgr as x:
syntax
Guaranteed cleanup on block exit.
@contextmanager
decorator
Turn a generator into a context manager.
ExitStack
class
Dynamically register multiple cleanups.
contextlib.suppress
class
Silently swallow specific exceptions.
contextlib.closing
class
Wrap any object with a close() method.
TemporaryDirectory
class
Auto-deleted temp directory.
threading.Lock
class
Use with 'with' to release on scope exit.
PEP 343
spec
The with statement.

Managing Resources Automatically code example

The script builds a timer context manager, uses ExitStack to open many files, and demonstrates cleanup on errors.

# Lesson: Managing Resources Automatically
import time
from contextlib import ExitStack, contextmanager
from pathlib import Path
from tempfile import TemporaryDirectory


@contextmanager
def timer(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        ms = (time.perf_counter() - start) * 1000
        print(f"[{label}] took {ms:.2f} ms")


class Connection:
    """Pretend database connection, for illustration."""
    def __init__(self, name: str):
        self.name = name
        self.open = True
        print(f"connect({name})")

    def close(self):
        self.open = False
        print(f"close({self.name})")

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        self.close()
        return False   # don't suppress exceptions


with timer("work"):
    time.sleep(0.01)

with Connection("db1") as c:
    print("using", c.name, "open?", c.open)
print("after block, open?", c.open)

# ExitStack: open an unknown number of files with guaranteed cleanup
with TemporaryDirectory() as tmp:
    paths = []
    for i in range(3):
        p = Path(tmp) / f"f{i}.txt"
        p.write_text(f"line {i}", encoding="utf-8")
        paths.append(p)

    with ExitStack() as stack:
        files = [stack.enter_context(open(p, "r", encoding="utf-8")) for p in paths]
        for f in files:
            print(f.name.split("/")[-1].split("\\")[-1], "=>", f.read())

# Cleanup even when an exception fires
try:
    with Connection("db2"):
        raise RuntimeError("boom")
except RuntimeError as err:
    print("caught:", err)

Key points:

1) `@contextmanager` + `try/finally` is the shortest real-world pattern.
2) A class with `__enter__`/`__exit__` works for richer cleanup.
3) `ExitStack` scales to a dynamic number of resources.
4) Cleanup runs even when the body raises — that's the whole point.

Write a suppress-and-log context manager.

from contextlib import contextmanager

@contextmanager
def ignore(*errors, log=None):
    try:
        yield
    except errors as err:
        if log is not None:
            log.append(repr(err))

log = []
with ignore(ValueError, log=log):
    int("xx")
print("log:", log)

Small behavior checks.

from contextlib import contextmanager
opened = []
@contextmanager
def scoped():
    opened.append("open")
    try: yield
    finally: opened.append("close")
with scoped(): opened.append("use")
assert opened == ["open", "use", "close"]

Output looks like:

[work] took 11.32 ms
connect(db1)
using db1 open? True
close(db1)
after block, open? False
f0.txt => line 0
f1.txt => line 1
f2.txt => line 2
connect(db2)
close(db2)
caught: boom