Context Managers
Learn how the with statement works, how to build custom context managers, and when to use them.
Context Managers
You have been using context managers since the file handling section:
with open("file.txt", "r") as f:
content = f.read()The with statement is clean and safe — the file closes automatically when the block ends, even if an exception occurs. But how does this actually work? And how do you build your own?
That is what this file covers.
The problem context managers solve
Without a context manager, you have to manage setup and cleanup manually:
file = open("data.txt", "r")
try:
content = file.read()
process(content)
finally:
file.close() # must always close — even if process() crashesThis works but it is verbose. Every resource you open needs this pattern. Miss the finally once and you have a resource leak.
The with statement wraps this pattern up:
with open("data.txt", "r") as f:
content = f.read()
process(content)
# file is closed here automatically — no matter whatSame safety. Half the code.
How with works under the hood
The with statement calls two special methods:
__enter__()— runs at the start of thewithblock, returns the value bound toas__exit__()— runs at the end of the block, always — whether it finished normally or crashed
Building a class-based context manager
Implement __enter__ and __exit__:
class ManagedFile:
def __init__(self, filepath, mode="r"):
self.filepath = filepath
self.mode = mode
self.file = None
def __enter__(self):
print(f"Opening {self.filepath}")
self.file = open(self.filepath, self.mode, encoding="utf-8")
return self.file # this is what 'as f' receives
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Closing {self.filepath}")
if self.file:
self.file.close()
return False # do not suppress exceptions
with ManagedFile("notes.txt") as f:
content = f.read()
print(content)Output:
Opening notes.txt
(file contents here)
Closing notes.txtThe three parameters of __exit__ carry exception information:
| Parameter | Meaning |
|---|---|
exc_type | The exception class — ValueError, TypeError, etc. |
exc_val | The exception instance — the actual error object |
exc_tb | The traceback object |
If no exception occurred, all three are None.
Returning True from __exit__ suppresses the exception:
def __exit__(self, exc_type, exc_val, exc_tb):
if self.file:
self.file.close()
if exc_type is FileNotFoundError:
print(f"File not found — handled.")
return True # suppress FileNotFoundError
return False # let everything else propagateOnly suppress exceptions deliberately and carefully. Suppressing an exception means the caller never knows something went wrong. Use it only when you have genuinely handled the problem.
A real class-based example — database connection
import sqlite3
class DatabaseConnection:
def __init__(self, db_path):
self.db_path = db_path
self.connection = None
self.cursor = None
def __enter__(self):
self.connection = sqlite3.connect(self.db_path)
self.cursor = self.connection.cursor()
return self.cursor
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
# no exception — commit the transaction
self.connection.commit()
else:
# exception occurred — roll back
self.connection.rollback()
print(f"Transaction rolled back due to: {exc_val}")
self.cursor.close()
self.connection.close()
return False
# clean usage
with DatabaseConnection("users.db") as cursor:
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER, name TEXT)")
cursor.execute("INSERT INTO users VALUES (1, 'Ali')")
cursor.execute("INSERT INTO users VALUES (2, 'Sara')")
# automatically committed and closedIf an exception happens inside the with block, the transaction rolls back automatically. If everything succeeds, it commits. The caller does not need to think about any of this.
Building with contextlib — the function-based way
Python's contextlib module gives you a decorator that turns a generator function into a context manager. This is often cleaner than writing a full class:
from contextlib import contextmanagerStructure:
@contextmanager
def my_context():
# setup — runs before the with block
print("Setting up")
try:
yield value # 'value' is what 'as x' receives
finally:
# cleanup — always runs
print("Cleaning up")Everything before yield is __enter__. Everything after is __exit__. The finally ensures cleanup always happens.
Simple example
from contextlib import contextmanager
@contextmanager
def managed_file(filepath, mode="r"):
print(f"Opening {filepath}")
f = open(filepath, mode, encoding="utf-8")
try:
yield f
finally:
f.close()
print(f"Closed {filepath}")
with managed_file("notes.txt") as f:
print(f.read())Output:
Opening notes.txt
(file contents)
Closed notes.txtReal example 1 — a timer context manager
Measure how long a block of code takes:
import time
from contextlib import contextmanager
@contextmanager
def timer(label=""):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f}s")
with timer("Sorting 1 million numbers"):
data = list(range(1_000_000, 0, -1))
data.sort()
with timer("Sum of squares"):
result = sum(x ** 2 for x in range(1_000_000))Output:
Sorting 1 million numbers: 0.0812s
Sum of squares: 0.0634sReal example 2 — temporary directory
Create a temporary directory, do work inside it, then clean it up automatically:
import shutil
import tempfile
from pathlib import Path
from contextlib import contextmanager
@contextmanager
def temp_directory():
path = Path(tempfile.mkdtemp())
print(f"Created temp dir: {path}")
try:
yield path
finally:
shutil.rmtree(path)
print(f"Deleted temp dir: {path}")
with temp_directory() as tmpdir:
# do work in the temporary directory
output_file = tmpdir / "results.txt"
output_file.write_text("Processing results...", encoding="utf-8")
print(f"Wrote to: {output_file}")
print(f"Contents: {output_file.read_text()}")
# temp directory is gone nowOutput:
Created temp dir: /tmp/tmpabc123
Wrote to: /tmp/tmpabc123/results.txt
Contents: Processing results...
Deleted temp dir: /tmp/tmpabc123Real example 3 — suppressing specific exceptions
contextlib has a built-in context manager for suppressing exceptions:
from contextlib import suppress
# instead of this
try:
import ujson as json
except ImportError:
import json
# you can write this
with suppress(ImportError):
import ujson as jsonAnother use — delete a file if it exists, ignore if it does not:
from contextlib import suppress
from pathlib import Path
with suppress(FileNotFoundError):
Path("temp_file.txt").unlink()No try/except needed. Clean and readable.
Real example 4 — redirecting stdout
contextlib can redirect print output to a file or string:
import io
from contextlib import redirect_stdout
# capture print output into a string
buffer = io.StringIO()
with redirect_stdout(buffer):
print("This goes to the buffer, not the screen")
print("So does this")
output = buffer.getvalue()
print(f"Captured: {output!r}")Output:
Captured: 'This goes to the buffer, not the screen\nSo does this\n'Useful for testing functions that print output.
Stacking context managers
You can use multiple context managers in one with statement:
# old way — nested
with open("input.txt", "r") as infile:
with open("output.txt", "w") as outfile:
outfile.write(infile.read())
# modern way — on one line
with open("input.txt", "r") as infile, open("output.txt", "w") as outfile:
outfile.write(infile.read())Both files are managed together. If either fails, both are cleaned up.
contextlib.ExitStack — dynamic context managers
Sometimes you do not know how many context managers you need until runtime. ExitStack handles this:
from contextlib import ExitStack
files = ["file1.txt", "file2.txt", "file3.txt"]
with ExitStack() as stack:
handles = [
stack.enter_context(open(f, "r", encoding="utf-8"))
for f in files
]
for handle in handles:
print(handle.read())
# all files closed hereYou push as many context managers as you need onto the stack. All of them get cleaned up when the with block ends.
When to write a context manager
| Situation | Write a context manager? |
|---|---|
| Opening and closing a resource | Yes |
| Setup and teardown that always go together | Yes |
| Temporarily changing state and restoring it | Yes |
| Measuring time of a block | Yes |
| Any pattern that needs guaranteed cleanup | Yes |
| Simple one-off code with no cleanup | No |
The core idea — any time you find yourself writing try/finally for cleanup, a context manager is usually the cleaner solution.
Summary
| Concept | Example |
|---|---|
| Use a context manager | with open(...) as f: |
| Class-based | Implement __enter__ and __exit__ |
| Function-based | @contextmanager with yield |
| Suppress exceptions | with suppress(ErrorType): |
| Stack multiple | with A() as a, B() as b: |
| Dynamic stacking | with ExitStack() as stack: |
Suppress in __exit__ | return True |
Propagate in __exit__ | return False |