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 hereYou 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