Back to Blog
Python

Python contextmanager decorator vs class context manager

python contextmanager decorator vs class context manager: Compare Python's @contextmanager decorator with class-based context managers: behavior, error handling, state...

contextlibcontext managerwith statementPython decoratorsresource management
Illustration comparing Python's @contextmanager decorator and class-based context manager with a yield and enter/exit symbols.

When you need to manage resources in Python, the with statement is the standard tool. Underneath it sits the context manager protocol, which you can implement in two distinct ways: by writing a class with __enter__ and __exit__ methods, or by using the @contextmanager decorator from contextlib to turn a generator function into a context manager. The choice between python contextmanager decorator vs class context manager affects how you handle exceptions, store state, and structure your code. This article explains the practical differences and gives you concrete criteria for picking one over the other.

The Two Ways to Write a Context Manager in Python

The with statement requires an object that implements the context manager protocol. That protocol consists of two methods: __enter__ is called when execution enters the block, and __exit__ is called when the block finishes, whether normally or via an exception. The __exit__ method receives the exception type, value, and traceback if an exception occurred, and it can suppress the exception by returning a truthy value.

The class-based approach implements these methods directly. The decorator-based approach uses a generator function with a yield statement. When you decorate such a function with @contextmanager, the code before yield runs inside __enter__, the value yielded becomes the result of the with expression, and the code after yield runs inside __exit__. Both approaches satisfy the same protocol, but they differ in how you express the logic.

How the Decorator Version Works

Here is a minimal @contextmanager implementation:

from contextlib import contextmanager @contextmanager def managed_file(path, mode): f = open(path, mode) try: yield f finally: f.close()

When you use with managed_file("data.txt", "r") as f:, the generator runs until the yield statement, which produces the file object. The with block executes, and when it finishes, the generator resumes after the yield. The finally block ensures the file is closed even if an exception occurs inside the with block.

The decorator handles the plumbing. It wraps your generator so that the __enter__ method calls next() on it, and the __exit__ method either calls next() again (if no exception) or throw() (if an exception occurred). The generator's finally block runs in both cases, which makes cleanup straightforward.

One key limitation: the generator must yield exactly once. If you try to yield multiple times, the context manager will raise a RuntimeError on the second entry. This makes the decorator approach unsuitable for context managers that need to be re-entered or that need to maintain state across multiple with blocks.

How the Class-Based Version Works

The class-based approach gives you full control over the protocol methods:

class ManagedFile: def __init__(self, path, mode): self.path = path self.mode = mode self.file = None def __enter__(self): self.file = open(self.path, self.mode) return self.file def __exit__(self, exc_type, exc_val, exc_tb): if self.file: self.file.close() # Return False to propagate exceptions, or True to suppress them return False

Here __enter__ does the setup and returns the resource object. __exit__ performs cleanup. The method signature gives you access to the exception details, and you can decide whether to suppress the exception by returning True or False. This is more explicit than the generator version, where exception suppression is handled differently.

Because the class is a regular object, you can store state on self and use it across multiple with blocks. You can also implement additional methods that the context manager exposes, making it a richer object than a simple generator wrapper.

Key Behavioral Differences

Exception Handling

The most significant difference lies in how each approach handles exceptions. In the class-based version, __exit__ receives the exception details and can decide to suppress it. If it returns True, the exception is swallowed and execution continues after the with block. If it returns False or None, the exception propagates.

In the decorator version, exceptions are handled by the generator's internal logic. If an exception occurs inside the with block, the decorator throws that exception into the generator at the yield point. This means the generator can catch it, but only within the try/except structure you write. To suppress an exception, you need to catch it inside the generator and avoid re-raising it. For example:

@contextmanager def suppress_errors(): try: yield except ValueError: pass

If you want the exception to propagate, you simply don't catch it. This is often more concise than the class-based approach, but it can be less obvious when the exception is intentionally swallowed.

State and Reusability

The class-based context manager is a persistent object. You can instantiate it once and use it multiple times, as long as __enter__ resets the state appropriately. This is useful for context managers that need to maintain configuration or accumulate data across invocations.

The decorator version is tied to a single generator execution. Once the generator finishes, it cannot be reused. Each with statement creates a new generator from the function call, so you cannot share state between different with blocks unless you use external variables. If you need to carry state, the class approach is more natural.

Return Value from __enter__

In the class version, __enter__ can return any object, including self if you want to expose methods on the context manager itself. In the decorator version, the value yielded is the return value of __enter__. You can yield any object, but you cannot easily return self because the generator is not the context manager object; the decorator wraps it. To expose methods, you would need to yield a separate object that carries those methods.

When to Use the Decorator Approach

The @contextmanager decorator shines for simple resource management where the setup and teardown are linear and you don't need complex state. Typical use cases include:

  • Opening and closing files, network connections, or database transactions.
  • Temporarily changing configuration or environment variables.
  • Acquiring and releasing locks.
  • Timing code blocks.

The decorator reduces boilerplate. You write a single function with a yield instead of a class with three methods. This is especially readable when the logic is short and the cleanup is deterministic. For example, a temporary directory context manager:

@contextmanager def temp_dir(): import tempfile, shutil path = tempfile.mkdtemp() try: yield path finally: shutil.rmtree(path)

This is concise and clear. The class version would require more lines to achieve the same effect.

When to Use the Class Approach

The class-based approach is the better choice when you need any of the following:

  • Persistent state across multiple with blocks: For example, a connection pool that tracks usage counts.
  • Custom __enter__ logic that depends on the instance state: You can pre-configure the object before entering the block.
  • Exposing methods on the context manager itself: If you want with manager as m: and then call m.some_method(), the class approach lets you return self from __enter__.
  • Fine-grained exception handling: When you need to inspect the exception type, value, and traceback separately and decide based on them, the explicit __exit__ signature is clearer.
  • Reusability: If you need to enter the same context manager multiple times, a class instance can be designed to be re-entrant, whereas a generator cannot.

Consider a context manager that measures time and accumulates statistics:

class Timing: def __init__(self): self.total_time = 0 def __enter__(self): import time self.start = time.perf_counter() return self def __exit__(self, exc_type, exc_val, exc_tb): self.total_time += time.perf_counter() - self.start return False

You can reuse the same Timing object across multiple with blocks, accumulating the total time. The decorator approach would require an external variable to hold the accumulated time, which is less encapsulated.

Performance and Overhead Considerations

Both approaches have negligible overhead for most applications, but there is a conceptual difference. The decorator version wraps your generator in a _GeneratorContextManager object, which adds a layer of indirection. The class version is a direct implementation of the protocol, so method calls go straight to your __enter__ and __exit__. In practice, the difference is micro-level and unlikely to matter unless you create and enter context managers in a tight loop with millions of iterations.

The generator version also has the overhead of yield and next() calls, but again, this is minimal. If you are writing performance-critical code, you should profile rather than assume one is faster. The real performance consideration is often the resource management itself, not the context manager wrapper.

Compatibility and Maintainability

The @contextmanager decorator is part of contextlib and has been available since Python 2.5, so it is widely compatible. The class-based approach requires no imports beyond the standard library. Both are supported in all modern Python versions.

From a maintainability perspective, the class approach is more explicit. The __enter__ and __exit__ methods clearly separate setup and teardown, and the exception handling is visible in the method signature. The decorator approach hides the control flow inside a generator, which can be less obvious to developers unfamiliar with the pattern. However, the decorator version is often shorter and easier to read for simple cases.

Testing also differs. A class-based context manager can be unit-tested by calling __enter__ and __exit__ directly, which gives you fine-grained control. The decorator version is typically tested through the with statement, though you can also call the generator manually. In practice, both are testable, but the class approach offers more direct access to the internal state.

Making the Choice: A Decision Guide

Use the @contextmanager decorator when:

  • The setup and teardown are linear and fit in a single function.
  • You don't need to maintain state between different with blocks.
  • You don't need to expose methods on the context manager object.
  • You want the shortest possible code.

Use the class-based approach when:

  • You need to store state on the context manager instance.
  • You want to reuse the same context manager object multiple times.
  • You need to implement custom exception suppression logic that depends on the exception details.
  • You want to return self from __enter__ to expose methods.
  • The context manager is complex enough that separating __enter__ and __exit__ improves readability.

There is no universal winner. The decision depends on the specific requirements of your resource management code. If you are unsure, start with the decorator for simple cases and switch to a class when you find yourself fighting the generator's limitations.

A final note on error handling: in the class version, returning True from __exit__ suppresses the exception. In the decorator version, you suppress by catching the exception inside the generator. If you need to suppress selectively based on the exception type, the class version's explicit exc_type parameter is more readable. If you simply want to ensure cleanup, the decorator's finally block is sufficient and often cleaner.

Both approaches are valid Python idioms. Understanding the behavioral differences allows you to choose the one that fits your code's structure and your team's readability preferences.

python contextmanager decorator vs class context manager: Pr | RYUSLOG DEV