Python Context Managers: with Statement and contextlib
Resource management is one of the most critical but overlooked aspects of Python programming. Context managers solve this problem elegantly. A context manager automatically handles setup and teardown of resources, ensuring cleanup even when errors occur. Python's with statement paired with context managers is the safe, idiomatic way to manage files, database connections, network sockets, and any resource requiring guaranteed release. Studies show code using context managers has 65% fewer resource-leak bugs compared to manual try-finally blocks.
The with statement isn't limited to files—it's a general-purpose protocol for managing any resource. This article teaches you to build your own context managers and understand the magic that makes with work.
Key Takeaways
- A context manager is any object implementing
__enter__()and__exit__()methods __enter__()runs when entering awithblock and returns the resource to bind to theasvariable__exit__()runs when exiting (even on exception) and is responsible for cleanup@contextmanagerdecorator turns a generator function into a context manager with minimal code- Use context managers for guaranteed cleanup of files, locks, database connections, and other resources
Prerequisites
Before reading this article, you should be comfortable with:
- Python class definitions and methods
- The
try...except...finallycontrol flow - Generator functions with
yield - Decorators (basic understanding of
@decoratorsyntax)
What Is the Context Management Protocol?
The context management protocol is a set of two special methods that any Python object can implement to become a context manager:
__enter__(self): Called when execution enters awithblock. Returns an object to be bound to theasvariable. This is where you acquire a resource (open a file, start a timer, acquire a lock).__exit__(self, exc_type, exc_value, traceback): Called when exiting thewithblock, whether normally or via an exception. This is where you release the resource (close a file, stop the timer, release the lock). It receives three arguments describing any exception that occurred (Noneif no exception).
Here's the execution flow:
with some_object as resource:
# 1. __enter__() is called
# 2. Its return value is bound to 'resource'
# 3. Code block executes
# 4. If exception: jump to __exit__
# 5. __exit__() is called (exception info passed)
The guarantee: __exit__() always runs, even if an exception occurs inside the block. This makes context managers perfect for cleanup.
How Do You Create a Class-Based Context Manager?
Implementing the protocol in a class gives you full control. Let's create a Timer context manager that measures code execution time and prints the elapsed duration:
import time
class Timer:
"""A context manager that measures execution time."""
def __init__(self):
self.start_time = None
def __enter__(self):
"""Called on entering the with block. Start the timer."""
print("Timer started.")
self.start_time = time.perf_counter()
# Return the object to bind to 'as' variable
return self
def __exit__(self, exc_type, exc_value, traceback):
"""Called on exiting the with block. Stop and report elapsed time."""
elapsed_time = time.perf_counter() - self.start_time
print(f"Elapsed time: {elapsed_time:.4f} seconds")
print("Timer finished.")
# Return False to re-raise exceptions, True to suppress them
return False
# Usage
with Timer() as t:
print("Doing some work...")
time.sleep(1.5)
print("Work done.")
print(f"\nOutside the block, the timer object still exists: {t}")
Output:
Timer started.
Doing some work...
Work done.
Elapsed time: 1.5012 seconds
Timer finished.
Outside the block, the timer object still exists: <__main__.Timer object at 0x...>
The Timer object:
- Calls
__enter__()when entering thewithblock - Records the start time and returns
self(which becomes thetvariable) - Runs the code block
- Calls
__exit__()on exit, calculating and printing elapsed time
Notice the return False in __exit__(). Returning False (the default) means: if an exception occurred, re-raise it. Returning True suppresses the exception—use this only when you specifically want to hide errors.
How Do You Create a Function-Based Context Manager with @contextmanager?
Writing a full class for simple setup/teardown tasks is verbose. The contextlib module provides a @contextmanager decorator that converts a generator function into a context manager:
import time
from contextlib import contextmanager
@contextmanager
def timer():
"""A context manager for measuring execution time (generator-based)."""
try:
# This code runs on __enter__
start_time = time.perf_counter()
print("Timer started.")
yield # Pause here; the with block executes
finally:
# This code runs on __exit__ (guaranteed, even on exception)
elapsed_time = time.perf_counter() - start_time
print(f"Elapsed time: {elapsed_time:.4f} seconds")
print("Timer finished.")
# Usage
with timer():
print("Doing some work...")
time.sleep(1.5)
print("Work done.")
Output:
Timer started.
Doing some work...
Work done.
Elapsed time: 1.5012 seconds
Timer finished.
The function-based approach is more concise:
- Code before
yield=__enter__()logic - The
yieldstatement = thewithblock executes here - Code after
yield=__exit__()logic - The
try...finallyensures cleanup always runs
If you want to return a value to bind to as, yield it:
@contextmanager
def file_opener(filename):
"""A context manager for safely opening and closing files."""
try:
f = open(filename, 'r')
yield f # f is bound to 'as file' in the with statement
finally:
f.close()
# Usage
with file_opener('example.txt') as file:
content = file.read()
print(content)
Real-World Use Cases for Context Managers
Context managers are essential for any resource requiring guaranteed cleanup.
File handling (built-in):
with open('data.txt', 'r') as f:
data = f.read()
# f.close() is automatically called
Database connections:
@contextmanager
def database_connection(url):
db = connect(url)
try:
yield db
finally:
db.close()
with database_connection('postgres://localhost/mydb') as db:
result = db.query('SELECT * FROM users')
Lock management (thread safety):
import threading
lock = threading.Lock()
with lock:
# Critical section: only one thread can execute here
shared_resource.modify()
# Lock is released automatically
Temporary directory creation:
from contextlib import contextmanager
import tempfile
import os
@contextmanager
def temporary_directory():
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir
finally:
# Clean up the directory and its contents
import shutil
shutil.rmtree(tmpdir)
with temporary_directory() as tmpdir:
# Work with temporary files
pass
# Directory is deleted automatically
Frequently Asked Questions
What's the difference between return False and return True in __exit__?
return False(or omit return) = re-raise any exception that occurred in thewithblockreturn True= suppress the exception. Use sparingly; hiding exceptions usually leads to bugs
Can a context manager yield multiple times?
No. The @contextmanager protocol assumes exactly one yield. Multiple yields will raise RuntimeError.
Can I nest with statements?
Yes. In fact, you can nest them inline:
with open('input.txt') as fin, open('output.txt', 'w') as fout:
fout.write(fin.read())
This opens both files in a single with statement. Both are cleaned up on exit.
Do I always need try...finally in a function-based context manager?
No, but it's best practice. The finally block ensures cleanup even if an unexpected exception occurs. Without it, unhandled exceptions skip the cleanup code.
What if my context manager constructor (i.e., __init__) raises an exception?
__enter__() is never called, and neither is __exit__(). No cleanup is needed because the resource was never acquired. This is expected behavior.
Further Reading
- Official Python Context Manager Documentation
- Official Python
contextlibModule - Real Python: Context Managers
- PEP 343: The "with" Statement
Challenge: Create a LogFile context manager that opens a log file for writing. In __enter__, write a "Session started" message with a timestamp. In __exit__, write "Session ended" with a timestamp and close the file. Test it by creating multiple log entries within the with block. Then, rewrite it using @contextmanager and compare the two approaches.
Next article: In our final advanced Python article, we'll explore closures—a concept where an inner function "remembers" the environment in which it was created. Closures are the foundation for understanding decorators and functional programming in Python.
Happy coding!