Context Managers and the with Statement
Context managers solve a fundamental problem in Python: making sure your cleanup code runs, no matter what. Whether you’re opening a file, connecting to a database, or locking a thread, you need guarantees that resources get released. The with statement gives you exactly that.
The problem context managers solve
Consider opening a file without a context manager:
f = open("data.txt", "w")
f.write("hello")
# What if an exception happens here?
f.close()
If an exception occurs before close(), the file stays open and data may not flush to disk. Worse, open file handles accumulate and can exhaust the operating system’s file descriptor limit under load. The fix is to guarantee close happens regardless of what goes wrong. You could wrap it in try/finally:
f = open("data.txt", "w")
try:
f.write("hello")
finally:
f.close()
This works, but it’s verbose. Every resource acquisition needs its own try/finally block, and nesting them makes the code hard to follow. A safer pattern is the context manager protocol, which Python exposes through the with statement.
The with statement
The with statement automatically calls __enter__ at the start and __exit__ at the end, even if an exception occurs:
with open("data.txt", "w") as f:
f.write("hello")
# File is automatically closed here
The as part captures whatever __enter__ returns. In this case, the file object itself.
The two-method protocol is straightforward: __enter__ sets up the resource and returns it, and __exit__ tears it down. Python calls __exit__ even if the code inside the block raises an exception, which is what makes the guarantee reliable. You can write your own context manager by implementing both methods on a class.
Building your own context manager
Create a class with __enter__ and __exit__ methods:
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self # optional: return something for the 'as' clause
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.time() - self.start
print(f"Elapsed: {self.elapsed:.4f} seconds")
return False # don't suppress exceptions
with Timer() as t:
sum(range(1000000))
# Elapsed: 0.0234 seconds
The __exit__ method receives exception details. Return True to suppress the exception, or False (default) to propagate it.
A timer is a simple example, but the real value of context managers shows up when the setup and teardown have side effects that must not be skipped. Database connections are the textbook case: open a connection at the start, commit on success, rollback on failure, and close in either case. Writing this logic by hand is error-prone, but a context manager bakes the correct sequence into the protocol.
Real-world example: database connection
import sqlite3
class DatabaseConnection:
def __init__(self, db_path):
self.db_path = db_path
self.conn = None
def __enter__(self):
self.conn = sqlite3.connect(self.db_path)
return self.conn
def __exit__(self, exc_type, exc_val, exc_tb):
if self.conn:
if exc_type is None:
self.conn.commit() # commit if no error
else:
self.conn.rollback() # rollback on error
self.conn.close()
return False
# Usage
with DatabaseConnection("app.db") as conn:
conn.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
# Connection closed, changes committed automatically
Using contextlib
Writing a class with __enter__ and __exit__ works everywhere, but it is a lot of boilerplate for simple cases. The contextlib module provides shortcuts so you do not need a full class. The two most useful are the @contextmanager decorator, which turns a generator function into a context manager, and closing, which wraps an object that has a close method.
contextmanager Decorator
from contextlib import contextmanager
import time
@contextmanager
def timer():
start = time.time()
try:
yield # This is where your code runs
finally:
elapsed = time.time() - start
print(f"Elapsed: {elapsed:.4f} seconds")
with timer():
sum(range(1000000))
The yield marks where the with block body executes. Code before yield runs in __enter__, code after in __exit__.
The decorator approach is ideal for one-off context managers where defining a class would feel heavy. The tradeoff is that the exception handling is less explicit: any exception raised inside the with block is re-raised at the yield point, so you must wrap yield in a try block if you need to react to errors. For objects that already have a close method and do not require custom logic, contextlib offers an even simpler tool.
closing()
Keep a context manager open when you just need cleanup:
from contextlib import closing
from urllib.request import urlopen
with closing(urlopen("https://example.com")) as page:
content = page.read()
# Connection closed automatically
The closing wrapper calls the object’s close method when the block exits, regardless of whether the block raised an exception. It is the simplest way to turn a legacy object with a close method into a context manager. When you do not need cleanup at all but want to suppress a specific exception, suppress is the right tool.
suppress()
Ignore specific exceptions:
from contextlib import suppress
import os
with suppress(FileNotFoundError):
os.remove("nonexistent.txt")
# No error if file doesn't exist
When to reach for context managers
Use context managers when you need to:
- Guarantee cleanup happens (files, connections, locks)
- Set up and tear down state
- Temporarily modify global state and restore it
- Measure execution time or resource usage
The with statement makes your code cleaner and safer. It moves cleanup out of your logic and into the protocol, where it’s harder to forget.
When to skip them
A context manager adds overhead. For simple cases where you don’t need guaranteed cleanup, a regular function call works fine. If you’re just transforming data and don’t need setup/teardown, skip it.
Stacking multiple managers
You can nest context managers or use multiple in a single with statement:
with open("input.txt") as infile, open("output.txt", "w") as outfile:
outfile.write(infile.read())
This is equivalent to nesting but reads more cleanly. Both files close properly when the block exits.
Summary
Context managers give you a reliable protocol for resource cleanup. The with statement guarantees your teardown code runs even when exceptions occur, so you spend less time writing try/finally blocks and more time on your actual logic. Use the class-based approach when you need full control over setup and teardown, or reach for contextlib decorators and wrappers when a full class would be overkill. The protocol works with files, database connections, locks, and any resource that needs a guaranteed cleanup step.
See also
- Error handling in Python — Exception handling patterns that pair with context managers
- Working with files in Python — File I/O where context managers are essential
- Python threading guide — Lock objects that use the context manager protocol