Two Ways to Define a Context Manager in Python 2026
Context managers are one of Python’s most elegant features for resource management. In 2026, there are two primary ways to create them: using a class with __enter__ and __exit__, or using the @contextmanager decorator. Understanding both approaches is essential for writing clean and robust functions.
TL;DR — Key Takeaways 2026
- Class-based context managers offer more control and are better for complex state
- The
@contextmanagerdecorator is simpler and more readable for most cases - Use
ExitStackwhen you need to manage a dynamic number of resources - Always ensure proper cleanup even when exceptions occur
1. Class-Based Context Manager
import sqlite3
def connect_to_database(db_url): # stand-in: SQLite instead of a real server
return sqlite3.connect(db_url)
class DatabaseConnection:
def __init__(self, db_url: str):
self.db_url = db_url
self.connection = None
def __enter__(self):
self.connection = connect_to_database(self.db_url)
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
if self.connection:
self.connection.close()
# Return False to let exceptions propagate
# Usage
with DatabaseConnection(":memory:") as conn:
result = conn.execute("SELECT 1 + 1").fetchone()
print(result) # (2,)
2. Decorator-Based Context Manager (@contextmanager)
from contextlib import contextmanager
@contextmanager
def timer(name: str):
"""Simple timer context manager."""
import time
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{name} took {elapsed:.4f} seconds")
def run_expensive_query(): # stand-in for a real database query
return sum(range(1_000_000))
# Usage
with timer("Database query"):
result = run_expensive_query()
3. When to Use Which Approach
- Use
@contextmanagerfor simple, short-lived resource management - Use class-based when you need complex state, multiple methods, or reusable objects
- Use
ExitStackwhen the number of context managers is dynamic
Class or Generator: Which Way to Choose
A context manager can be written as a class with __enter__ and __exit__, or as a generator function decorated with contextlib.contextmanager. Both give the same with behaviour, so the choice is about readability and features.
The generator form is shorter. Code before yield is the set-up, the yielded value is bound by as, and code after yield, placed in a finally block, is the clean-up. It suits simple set-up and tear-down pairs.
The class form gives more control. __exit__ receives the exception type, value and traceback, and returning True from it suppresses the exception. A class can also keep state between uses and offer extra methods.
In both forms, put clean-up in a place that runs even when the block fails: finally in the generator form, and __exit__ in the class form.
Worked Example: The Same Manager Written Twice
We write a manager that temporarily changes a setting in a dictionary, once as a class and once as a generator.
from contextlib import contextmanager
settings = {"debug": False}
class Override:
def __init__(self, key, value):
self.key, self.value = key, value
def __enter__(self):
self.old = settings[self.key]
settings[self.key] = self.value
def __exit__(self, exc_type, exc, tb):
settings[self.key] = self.old
return False # do not hide errors
@contextmanager
def override(key, value):
old = settings[key]
settings[key] = value
try:
yield
finally:
settings[key] = old
with Override("debug", True):
print("class:", settings)
try:
with override("debug", True):
raise RuntimeError("boom")
except RuntimeError:
print("after error:", settings)
Inside the class version debug is True, and both versions restore it to False afterwards, even when the block raises an error. Because neither suppresses the exception, the RuntimeError still reaches the caller.
Practical Tips
Start with the generator form for most new context managers. Switch to a class only when you need exception details in __exit__, extra methods or state shared across uses.
Test both the normal path and the error path, and check that clean-up runs in each case. A small test that raises inside the block is enough.
Common Mistakes
- Leaving clean-up after yield without try/finally.
- Returning True from __exit__ by accident and hiding exceptions.
- Yielding more than once in a contextmanager generator.
- Choosing a class when a few lines of generator would do.
Quick Summary
- Two forms: class or @contextmanager generator.
- Generators suit simple pairs.
- Classes give access to exception details.
- Always make clean-up run on errors.
Conclusion
Both class-based and decorator-based context managers have their place in modern Python development. In 2026, choosing the right approach helps you write cleaner, safer, and more maintainable code when working with resources.
Next steps:
- Review your resource management code and refactor using the most appropriate context manager pattern