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:
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 wrapperThe 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:
def collect_results(results=[]):
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
results.append(result)
return result
return wrapper
return decoratorThe 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:
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 FalseIf 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:
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 FalseThe 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:
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.cleanup()
return FalseThis 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:
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
self.rollback()
self.cleanup()
return FalseThe 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
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.
Frequently Asked Questions
What is the most common mistake when writing Python decorators?
Why does my context manager not suppress exceptions even when returning True from __exit__?
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.
More in this topic
Create Custom Iterators in Python
Learn how to build your own iterators in Python by implementing the __iter__() and __next__() methods, with practical examples and reusable patterns.
Python Iterator Protocol Explained
Learn how Python's iterator protocol works with the __iter__() and __next__() methods, and understand the contract every iterator must follow.
Python Iterables and Iterators Explained
Learn what iterables and iterators are in Python, how they power for loops under the hood, and why understanding them unlocks cleaner data processing patterns.