Python by Example

Context Managers

Context managers pair setup and teardown in a with block. Use pathlib + with for files, contextlib.contextmanager for one-offs.

A context manager is an object with two special methods: __enter__ (runs when the with block starts) and __exit__ (runs when the block ends, whether normally or via an exception). The classic example is a file handle: with open("...") as f: - __exit__ closes the file even if the body raises, so you never forget cleanup.

pathlib.Path is the modern way to work with file paths in Python 3.4+. Calling .open() on a Path object returns a file handle that works exactly like the built-in open(). Wrapping it in with guarantees the file is closed when the block exits - no matter what happens inside. This replaces the older os.path + bare open() pattern.

from pathlib import Path
 
p = Path("data.txt")
 
# Write something first
p.write_text("hello, world\n")
 
# Read it back - file is closed automatically when with block exits
with p.open() as f:
    data = f.read()
 
print(data)  # hello, world
 
# Works the same even if the body raises - __exit__ still closes the file
try:
    with p.open() as f:
        content = f.read()
        raise ValueError("something went wrong mid-read")
except ValueError:
    pass  # file is already closed here

You can write your own context manager by adding __enter__ and __exit__ methods to a class. __enter__ runs first and returns the value that with ... as x: binds to x. __exit__ receives three arguments about any exception that occurred (exc_type, exc_val, traceback) - returning a falsy value (or None) lets the exception propagate; returning a truthy value suppresses it.

import time
 
 
class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self  # bound to the 'as' variable
 
    def __exit__(self, exc_type, exc_val, traceback):
        self.elapsed = time.perf_counter() - self.start
        return None  # do not suppress exceptions
 
 
def slow_work():
    total = sum(range(1_000_000))
    return total
 
 
with Timer() as t:
    result = slow_work()
 
print(f"Result: {result}")
print(f"Elapsed: {t.elapsed:.4f}s")
# Result: 499999500000
# Elapsed: 0.0312s  (varies by machine)

Writing a full class for a one-off context manager is often more code than necessary. contextlib.contextmanager lets you write the same logic as a generator function: code before yield is the setup (__enter__), the yielded value is what with ... as x: receives, and code after yield is the teardown (__exit__). Wrap teardown in try/finally so it runs even when the body raises.

import os
from contextlib import contextmanager
 
 
@contextmanager
def chdir(path):
    """Temporarily change working directory."""
    old = os.getcwd()
    os.chdir(path)
    try:
        yield  # caller's with-block runs here
    finally:
        os.chdir(old)  # always restore, even on exception
 
 
# Usage
print(os.getcwd())          # /some/original/path
 
with chdir("/tmp"):
    print(os.getcwd())      # /tmp
    # do work here
 
print(os.getcwd())          # /some/original/path  (restored)

In production

A context manager whose __exit__ returns a truthy value silently suppresses the exception - this is one of the most common sources of swallowed errors in production; __exit__ should return None (or False) unless suppression is explicitly intended. Reach for contextlib.contextmanager over a class for one-off cases - the value you yield is what with ... as x: binds, and cleanup runs in the finally after yield. pathlib.Path.open() combined with with is the modern file-I/O idiom - never call open() without with, and prefer pathlib over os.path in new code.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free