Common Python Decorator and Context Manager Mistakes

Learn the most common mistakes with Python decorators and context managers, including missing functools.wraps, mutable defaults, exception suppression, and resource leaks.

8 min read

Python decorators and context managers are two of the language's most elegant abstractions, but they also have well-known failure modes that catch even experienced developers. This article catalogs the most common Python decorator and context manager mistakes, drawing on patterns that appear repeatedly in code reviews, bug reports, and production incidents. Each mistake is presented with the incorrect code, an explanation of why it fails, and the corrected version. Understanding these failure modes will help you write robust decorators and context managers from the start and recognize problematic patterns when you encounter them in existing code.

The mistakes in this article are drawn from the patterns covered throughout this section. If you have been following the progression from Python decorators explained through creating custom context managers, you have seen the correct versions of each pattern. This article focuses on what goes wrong when the correct patterns are not followed. The goal is not to discourage the use of decorators and context managers (they are essential tools in any Python developer's toolkit) but to equip you with the knowledge to use them correctly and debug them when they misbehave.

Forgetting functools.wraps

The most common decorator mistake, and the one with the subtlest symptoms, is forgetting to apply functools.wraps to the wrapper function. Without wraps, the decorated function takes on the wrapper's metadata: its name becomes "wrapper", its docstring is lost, and its module and qualified name reflect the decorator's location rather than the original function's. The function still works when called, but every tool that inspects it sees wrong information.

The incorrect version looks like every decorator you wrote before learning about wraps:

pythonpython
import time
 
def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

The timing output still shows the correct function name because the wrapper reads the closure variable. But the decorated function's identity is wrong. The fix is a single line, adding the wraps decorator covered in preserving function metadata with functools.wraps on the line above the wrapper definition. It copies the original function's name, docstring, module, qualified name, annotations, and type parameters onto the wrapper, and sets a reference to the original for unwrapping.

Mutable default arguments in decorator factories

Decorator factories that accept parameters sometimes use mutable default arguments for those parameters. This is the same mutable-default-argument problem that affects regular functions, but it manifests differently in decorators because the factory runs once per decoration, not once per call. The incorrect pattern looks like this:

pythonpython
def collect_results(results=[]):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            results.append(result)
            return result
        return wrapper
    return decorator

The symptom is confusing: every function decorated with a bare collect_results call appends into the same shared list, because Python evaluates the default argument once when the factory is defined, not once per decoration. Results from unrelated functions mix together, and tests that inspect the collection pass or fail depending on which other tests ran first. The fix creates the mutable object inside the factory body instead of in the default argument, using the None-default pattern applied one level up in the decorator factory. Each call to the factory with no arguments creates a fresh list, so functions decorated separately get independent result collections.

Confusing definition time and call time

Decorators execute at definition time, when the module is imported. Code in the decorator body outside the wrapper runs once per decorated function, during import. Code inside the wrapper runs every time the decorated function is called. Confusing these two execution times leads to decorators that perform expensive work at import time or that capture state that does not yet exist.

The incorrect pattern puts expensive initialization in the decorator body, outside the wrapper. A decorator that reads a configuration file might call load_config_from_file at import time. If the file does not exist then, the import fails. If the configuration changes while the program runs, the decorator never sees new values. The fix moves initialization into the wrapper with lazy loading: store a None sentinel, check it on first call, and load the configuration once. This defers work until it is needed and allows the program to start without the configuration being immediately available.

Forgetting to handle enter failures in context managers

A context manager's exit method is only called after the enter method returns successfully. If enter raises an exception, the with statement never runs the block and never calls exit, because enter is invoked before the try-finally structure that protects the block. Developers often assume exit is a catch-all cleanup hook, but a failure halfway through enter leaks anything acquired up to that point. The incorrect pattern performs multi-step setup in enter with no protection:

pythonpython
class ManagedResource:
    def __enter__(self):
        self.resource = acquire_resource()
        self.resource.initialize()
        return self.resource
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.resource.release()
        return False

If acquire_resource succeeds but initialize raises an exception, the exception propagates out of enter, exit is never called, and the acquired resource leaks with no cleanup path at all. The fix is for enter to clean up after itself when a later setup step fails:

pythonpython
class ManagedResource:
    def __enter__(self):
        self.resource = acquire_resource()
        try:
            self.resource.initialize()
        except Exception:
            self.resource.release()
            raise
        return self.resource
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.resource.release()
        return False

The try-except inside enter releases the already-acquired resource and re-raises the exception, so a partial setup never leaks. Once enter returns successfully, the with statement guarantees that exit runs, so the release in exit covers every other path: normal completion, exceptions in the block, and early returns. The rule to remember is that enter owns cleanup for its own failures, and exit owns cleanup for everything after.

Resource leaks from missing cleanup paths

Context managers guarantee that exit runs, but exit itself must be implemented correctly to actually release the resource. A common mistake is to put the cleanup logic in a place that is only reached on the normal exit path, not the exceptional one. The incorrect pattern uses a bare except or an else clause that skips cleanup on certain exception types:

pythonpython
def __exit__(self, exc_type, exc_val, exc_tb):
    if exc_type is None:
        self.cleanup()
    return False

This exit method only cleans up when no exception occurred. If the block raises an exception, cleanup is skipped entirely, and the resource leaks. The fix ensures that cleanup runs unconditionally: in the simplest case, exit contains nothing but the cleanup call and a return False. If you need different cleanup for success and failure paths, put the common cleanup outside the conditional and the path-specific logic inside it:

pythonpython
def __exit__(self, exc_type, exc_val, exc_tb):
    if exc_type is not None:
        self.rollback()
    self.cleanup()
    return False

The rollback runs only on failure, but the cleanup runs on both paths. The resource is always released, and the exception decision is separate from the resource management.

The final article in this section brings everything together: building a resource management utility in Python that combines decorators and context managers to manage file handles, database connections, and temporary resources with consistent cleanup guarantees.

Rune AI

Rune AI

Key Insights

  • Always use functools.wraps on decorator wrappers to preserve the original function's metadata.
  • Never use mutable default arguments in decorator factories; create fresh state inside the decorator function instead.
  • Remember that decorators run at definition time, not call time, so expensive setup belongs inside the wrapper.
  • If enter raises, exit never runs, so enter must release anything it already acquired before letting the exception propagate.
  • Keep cleanup in exit unconditional, and branch only for path-specific logic like rollback.
RunePowered by Rune AI

Frequently Asked Questions

What is the most common mistake when writing Python decorators?

The most common mistake is forgetting to use functools.wraps on the wrapper function. Without wraps, the decorated function's __name__ becomes 'wrapper', its docstring is lost, and introspection tools see the wrong metadata. Another very common mistake is using mutable default arguments in the decorator factory, which causes state to be shared across all decorated functions instead of being independent per function.

Why does my context manager not suppress exceptions even when returning True from __exit__?

If __exit__ returns True, the exception should be suppressed, but there are two common reasons it might not work. First, the __exit__ method might not be returning True on the correct code path. Check that your return statement is reached when the exception type you want to suppress occurs. Second, if the __exit__ method itself raises an exception, that new exception replaces the original one and propagates regardless of the return value.

Conclusion

Decorators and context managers are powerful abstractions, but they have sharp edges. Forgetting wraps, sharing mutable state, misunderstanding definition-time execution, mishandling exceptions in exit, and leaking resources are the mistakes that developers encounter most often. Learning to recognize these patterns in your own code and in code you review is the difference between using these features and mastering them.