Python Course
Context Managers
Every time you open a file with with open(...) as f:, you are using a context manager. Context managers guarantee that setup and teardown logic runs reliably — even when exceptions occur. They are the cleanest solution to the resource management problem: ensuring that files are closed, database connections are released, locks are freed, and temporary state is restored, no matter what happens inside the block.
This lesson covers how the with statement works, how to build context managers using classes and the contextlib module, and the real-world scenarios where they save you from subtle, hard-to-debug resource leaks.
The Problem Context Managers Solve
Without a context manager, resource cleanup depends on the programmer remembering to do it — and on no exception occurring before the cleanup code runs.
# Without a context manager — fragile cleanup
f = open("data.txt", "w")
f.write("some data")
f.close() # fine if nothing goes wrong
# But if an exception occurs before close(), the file stays open
f = open("data.txt", "w")
try:
f.write("some data")
raise ValueError("something went wrong") # simulated error
finally:
f.close() # must wrap in try/finally to guarantee cleanup
print("File closed in finally block")The with statement replaces the try/finally pattern with a single clean line — and the teardown is guaranteed automatically.
How the with Statement Works
The with statement relies on two magic methods: __enter__ runs at the start of the block and __exit__ runs at the end — guaranteed, even if an exception occurs. The object that implements these two methods is the context manager.
# The with statement — clean, guaranteed resource management
# Using the built-in file context manager
with open("data.txt", "w") as f:
f.write("Hello from context manager!")
# f.close() is called automatically here — even if an exception occurred
# Reading it back
with open("data.txt", "r") as f:
content = f.read()
print(content)
# Nesting context managers
with open("input.txt", "w") as inp, open("output.txt", "w") as out:
inp.write("input data")
out.write("output data")
print("Both files written and closed automatically.")Both files written and closed automatically.
- The
as fpart binds the value returned by__enter__to the variablef - Multiple context managers can be combined on one line with a comma
__exit__is always called — even if an exception is raised inside the block
Building a Class-Based Context Manager
Any class that implements __enter__ and __exit__ is a context manager. This gives you full control over setup, teardown, and exception handling.
Why it exists: the class-based approach is ideal when the context manager needs to maintain state or when the setup and teardown logic is complex enough to deserve its own class.
Real-world use: a database connection class implements __enter__ to open the connection and __exit__ to commit or roll back the transaction and close the connection — all guaranteed regardless of what happens inside the block.
# Class-based context manager — __enter__ and __exit__
class ManagedFile:
"""A context manager that opens and closes a file."""
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
def __enter__(self):
self.file = open(self.filename, self.mode)
print(f"Opened: {self.filename}")
return self.file # bound to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
print(f"Closed: {self.filename}")
if exc_type:
print(f"Exception suppressed: {exc_val}")
return True # True suppresses any exception
with ManagedFile("notes.txt", "w") as f:
f.write("context managers are great")
raise ValueError("oops!") # exception raised inside block
print("Execution continues — exception was suppressed")Closed: notes.txt
Exception suppressed: oops!
Execution continues — exception was suppressed
__exit__receives three arguments:exc_type,exc_val,exc_tb— allNoneif no exception occurred- Return
Truefrom__exit__to suppress the exception; returnFalse(or nothing) to let it propagate __enter__returns the value that gets bound to the variable afteras- In most real-world cases you want exceptions to propagate — only suppress when you have a specific reason
Building a Context Manager with contextlib
Writing a full class for a simple context manager is often overkill. The contextlib.contextmanager decorator lets you write a context manager as a generator function — everything before yield is setup, yield is the value bound to as, and everything after is teardown.
Why it exists: it eliminates the class boilerplate for straightforward context managers while keeping the setup/teardown guarantee.
# contextlib.contextmanager — generator-based context manager
from contextlib import contextmanager
@contextmanager
def managed_file(filename, mode):
print(f"Opening: {filename}")
f = open(filename, mode)
try:
yield f # value bound to the 'as' variable
finally:
f.close() # always runs — setup in try, teardown in finally
print(f"Closed: {filename}")
with managed_file("notes.txt", "w") as f:
f.write("generator-based context manager")
print("Writing done.")Writing done.
Closed: notes.txt
- The
try/finallyinside the generator ensures teardown runs even if the block raises an exception - Only one
yieldis allowed — it marks the boundary between setup and teardown - This is the preferred pattern for simple context managers — less code, same guarantees
Practical Example — Timer Context Manager
A timer context manager measures how long a block of code takes to execute — cleaner than manually recording start and end times around every block you want to measure.
# Timer context manager — measures block execution time
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} took {elapsed:.4f}s")
with timer("Sum of range"):
result = sum(range(5_000_000))
print("Result:", result)
with timer("List comprehension"):
squares = [x**2 for x in range(1_000_000)]Sum of range took 0.1823s
List comprehension took 0.0741s
Practical Example — Temporary Directory Change
A context manager that temporarily changes the working directory and restores it afterwards — no matter what happens inside the block.
# Temporary working directory context manager
import os
from contextlib import contextmanager
@contextmanager
def change_dir(path):
original = os.getcwd() # save current directory
os.chdir(path) # switch to new directory
print(f"Changed to: {os.getcwd()}")
try:
yield
finally:
os.chdir(original) # always restore original directory
print(f"Restored to: {os.getcwd()}")
print("Before:", os.getcwd())
with change_dir("/tmp"):
print("Inside block:", os.getcwd())
print("After:", os.getcwd())Changed to: /tmp
Inside block: /tmp
Restored to: /home/user/project
After: /home/user/project
- The original directory is restored even if an exception is raised inside the block
- This pattern works for any "temporarily change something and restore it" scenario — environment variables, configuration settings, mock patches
contextlib Utilities Worth Knowing
The contextlib module ships several ready-made context managers that handle common patterns.
# Useful contextlib utilities
from contextlib import suppress, redirect_stdout, nullcontext
import io
# suppress — silently ignore specific exceptions
with suppress(FileNotFoundError):
open("nonexistent_file.txt") # no error raised
print("suppress: carried on after missing file")
# redirect_stdout — capture print output into a string
buffer = io.StringIO()
with redirect_stdout(buffer):
print("this goes to the buffer, not the terminal")
captured = buffer.getvalue()
print("captured:", captured.strip())
# nullcontext — a no-op context manager (useful in conditional code)
debug_mode = False
ctx = timer("block") if debug_mode else nullcontext()
with ctx:
x = sum(range(100))captured: this goes to the buffer, not the terminal
suppress(*exceptions)— cleanest way to intentionally swallow a specific exceptionredirect_stdout(buffer)— useful in testing to capture output without mockingnullcontext()— a context manager that does nothing, useful when a context manager is optionalcontextlib.closing(obj)— wraps any object with aclose()method in a context manager
Summary Table
| Tool | Approach | Best Used For |
|---|---|---|
__enter__ / __exit__ |
Class-based | Complex setup/teardown, stateful managers |
@contextmanager |
Generator-based | Simple managers — less boilerplate |
suppress() |
contextlib utility | Intentionally swallowing specific exceptions |
redirect_stdout() |
contextlib utility | Capturing print output in tests |
nullcontext() |
contextlib utility | Optional context manager in conditional code |
Practice Questions
Practice 1. What two magic methods must a class implement to work as a context manager?
Practice 2. What does returning True from __exit__ do?
Practice 3. In a @contextmanager generator, what does yield mark?
Practice 4. Which contextlib utility silently ignores a specific exception type?
Practice 5. Is __exit__ called when an exception occurs inside the with block?
Quiz
Quiz 1. What is the main advantage of using a context manager over a plain try/finally block?
Quiz 2. What value is bound to the variable after as in a with statement?
Quiz 3. In a @contextmanager generator, why should the yield be inside a try/finally?
Quiz 4. What are the three arguments __exit__ receives when an exception occurs?
Quiz 5. Which approach is preferred for simple context managers that do not need state?
Next up — Multithreading: running tasks concurrently using Python's threading module.