Python Context Managers and the with Statement
A practical guide to Python's with statement and context managers — why they exist, how to write your own with __enter__ and __exit__, and how to use contextlib for one-liners.
What you'll learn
- ✓Why the with statement exists
- ✓How __enter__ and __exit__ work
- ✓Writing a class-based context manager
- ✓Using contextlib.contextmanager for simpler cases
- ✓Combining multiple context managers
Prerequisites
- •Basic Python familiarity
A context manager is any object that knows how to set something up and tear it down again — opening and closing a file, acquiring and releasing a lock, beginning and committing a database transaction. The with statement is the syntax Python uses to drive that lifecycle so you cannot forget the teardown step.
The Problem with Manual Cleanup
Consider opening a file without with.
f = open("data.txt")
data = f.read()
f.close()
This usually works. It breaks the moment f.read() raises an exception, because f.close() is skipped and the file handle leaks until the garbage collector eventually closes it. The fix is try/finally.
f = open("data.txt")
try:
data = f.read()
finally:
f.close()
That works but it is verbose and easy to get wrong. The with statement bakes the same pattern into a one-liner.
with open("data.txt") as f:
data = f.read()
When the block exits — normally or via exception — the file is closed. That’s it.
How with Actually Works
A context manager is just an object that implements two dunder methods: __enter__ and __exit__. The with statement calls __enter__ at the top of the block and __exit__ when the block ends.
Here is a minimal example.
class Loud:
def __enter__(self):
print("entering")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("exiting")
return False # don't swallow exceptions
with Loud() as ctx:
print("inside block")
The output is entering, inside block, exiting. If you raise inside the block, __exit__ still runs.
The three arguments to __exit__ describe an exception that occurred inside the block (or are all None if everything went fine). Returning True from __exit__ swallows the exception, which is rarely what you want — return False or simply nothing.
A Realistic Example: a Timer
A class-based context manager for timing a block of code.
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
print(f"Block took {self.elapsed:.4f}s")
with Timer() as t:
sum(range(1_000_000))
After the block, t.elapsed holds the duration. Notice how __enter__ returns self, which is what as t captures.
The contextlib Shortcut
For simple managers, writing a whole class is overkill. The contextlib.contextmanager decorator lets you build one from a generator.
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"Block took {elapsed:.4f}s")
with timer():
sum(range(1_000_000))
Code before yield runs as __enter__. Code after yield (in the finally block) runs as __exit__. Whatever you yield becomes the value bound by as.
This style is shorter, easier to read, and perfect for cases that don’t need their own class.
Managing Other Resources
Context managers shine far beyond files. Locks, database connections, temporary directories, and even mocks in tests all expose context manager interfaces.
import threading
lock = threading.Lock()
with lock:
# critical section — lock released automatically
shared_counter += 1
import tempfile
with tempfile.TemporaryDirectory() as tmp:
# tmp is a path; the directory and its contents are deleted on exit
...
The pattern is consistent: acquire on entry, release on exit, no matter what happens in between.
Combining Multiple Managers
You can stack managers in one with statement using commas.
with open("input.txt") as src, open("output.txt", "w") as dst:
for line in src:
dst.write(line.upper())
Both files are guaranteed to close. If you need to manage a dynamic number of managers, contextlib.ExitStack lets you push them on programmatically.
from contextlib import ExitStack
paths = ["a.txt", "b.txt", "c.txt"]
with ExitStack() as stack:
files = [stack.enter_context(open(p)) for p in paths]
# all files closed when the with block exits
Suppressing Exceptions Cleanly
contextlib.suppress is a tiny context manager that swallows a specific exception type. It is cleaner than an empty except block.
from contextlib import suppress
import os
with suppress(FileNotFoundError):
os.remove("maybe-here.txt")
If the file doesn’t exist, nothing happens. If it does, it gets removed.
When to Reach for a Custom Context Manager
Any time you have a resource that needs deterministic cleanup, or a paired setup/teardown action — opening a network connection, starting and stopping a profiler, switching directories, patching configuration during a test — a context manager makes the intent obvious and the cleanup automatic.
Wrapping Up
with is one of Python’s most underrated features. Once you start writing your own context managers — either as classes or via contextlib.contextmanager — you stop worrying about forgotten cleanup and your code reads as a list of named scopes instead of a maze of try/finally.
Related articles
- Python Python Context Managers and the with Statement
Learn how Python context managers work, how to write them with __enter__/__exit__ and contextlib, and how to use async context managers in real code.
- Python Python asyncio Event Loop Guide
Understand how Python's asyncio event loop schedules coroutines, what await actually does, and how to avoid the classic mistakes that turn async code into a tangle of bugs.
- Python Python Decorators Deep Dive
A practical tour of Python decorators: how they work under the hood, when to use them, and how to write decorators that preserve metadata, accept arguments, and stack cleanly.
- Python Python Logging Best Practices
How to set up Python logging properly: loggers vs handlers, structured logs, contextual fields, log levels that scale, and how to avoid the classic print-debug trap.