Two ways to define a context manager in Python give you flexibility to manage resources safely with the with statement: class-based (full control via __enter__ and __exit__) or generator-based (simpler, using @contextmanager and yield). Both ensure setup runs on entry and cleanup runs on exit — even if exceptions occur — preventing leaks of files, connections, locks, transactions, or temporary state. In 2026, both styles are widely used: class-based for complex logic or reusable classes, generator-based for quick, readable, one-off managers. Choosing the right style keeps code clean, exception-safe, and maintainable in data pipelines, scraping, concurrency, testing, and production systems.
Here’s a complete, practical guide to the two ways to define context managers in Python: class-based vs generator-based, when to use each, advanced features, real-world patterns, and modern best practices with type hints, multiple contexts, ExitStack, and pandas/Polars integration.
Class-based context manager — implement __enter__ (setup, return resource) and __exit__ (cleanup, optional exception handling).
class DatabaseConnection:
def __init__(self, db_url: str):
self.db_url = db_url
self.conn = None
def __enter__(self):
self.conn = connect_to_db(self.db_url) # setup
return self.conn # return to 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
if self.conn:
self.conn.close() # cleanup
if exc_type is not None:
print(f"Exception occurred: {exc_val}")
return False # re-raise exception (don't suppress)
# Usage
with DatabaseConnection("sqlite:///mydb.db") as conn:
results = conn.execute("SELECT * FROM users")
# conn closed automatically, even on exception
Generator-based context manager (recommended for most cases) — use @contextmanager from contextlib: yield the resource, code before yield is setup, after yield is cleanup.
from contextlib import contextmanager
@contextmanager
def temp_file(content: str):
import tempfile
import os
with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp:
tmp.write(content)
tmp.flush()
yield tmp.name # provide path to user
# cleanup after yield block exits
os.unlink(tmp.name)
# Usage
with temp_file("Hello, temp!") as path:
with open(path) as f:
print(f.read()) # Hello, temp!
# file deleted automatically
Real-world pattern: nested or multiple contexts in pandas/Polars pipelines — ensure file, connection, and transaction cleanup at every level.
@contextmanager
def db_connection(db_path: str):
conn = sqlite3.connect(db_path)
try:
yield conn
finally:
conn.close()
@contextmanager
def transaction(conn):
cur = conn.cursor()
try:
yield cur
conn.commit()
except Exception:
conn.rollback()
raise
finally:
cur.close()
# Nested usage
with db_connection('data.db') as conn:
with transaction(conn) as cur:
df = pd.read_sql("SELECT * FROM sales", conn)
# process df...
cur.execute("UPDATE sales SET processed=1 WHERE id > 100")
# both closed and committed/rolled back automatically
Best practices make context managers safe, readable, and performant. Prefer generator-based (@contextmanager) over class-based — simpler, less boilerplate, automatic exception handling. Use single-line multiple with — with A() as a, B() as b: — cleaner than deep nesting. Modern tip: use Polars for file/DB I/O — pl.read_database(...) with context managers for connections. Add type hints — def func() -> ContextManager[Resource] — improves clarity and mypy checks. Use contextlib.ExitStack for dynamic/variable contexts — add resources conditionally. Use contextlib.suppress() for ignoring specific exceptions. Use contextlib.redirect_stdout/redirect_stderr for logging redirection. Handle exceptions properly — let them propagate unless suppression needed (return True in __exit__). Test context managers — assert resource released after block with assert not resource.open. Nest judiciously — too deep nesting reduces readability; refactor to functions or ExitStack when complex.
Two ways to define context managers — class-based (__enter__/__exit__) for full control, generator-based (@contextmanager + yield) for simplicity. In 2026, prefer generator-based, use multiple/single-line with, ExitStack for dynamic nesting, type hints, and Polars for I/O. Master both, and you’ll manage files, connections, locks, and transactions reliably — no leaks, no deadlocks, no partial states.
Next time you need to manage resources — define a context manager. It’s Python’s cleanest way to say: “Acquire this, use it, and guarantee cleanup — no matter what.”