Two Ways to Define a Context Manager in Python 2026

Writing Functions Mar 16, 2026

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 @contextmanager decorator is simpler and more readable for most cases
  • Use ExitStack when 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 @contextmanager for simple, short-lived resource management
  • Use class-based when you need complex state, multiple methods, or reusable objects
  • Use ExitStack when 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

Related Tutorials

Share:
Last updated: March 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices