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. 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. Enter context managers.
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.
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.
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
The contextlib module provides shortcuts so you don’t need a full class.
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__.
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
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 Use 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 Not to Use 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.
Combining Context 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 provide a clean way to handle setup and teardown logic. The with statement guarantees your cleanup code runs, even when exceptions occur. Use the class-based approach for complex state management, or reach for contextlib decorators when you need something quicker.