Python Decorator vs Closure: What's the Difference?
python decorator vs closure: Understand how Python closures capture state and how decorators build on that mechanism to wrap callables, with practical examples and tra...
When comparing python decorator vs closure, the practical question is not which one to choose, but how the two concepts relate. A decorator is a higher-order function that wraps another function, and it typically relies on a closure to preserve access to the wrapped function and its arguments. A closure, by itself, is a nested function that captures variables from its enclosing scope. Understanding that relationship clarifies when you are writing a decorator, when you are writing a closure, and when you are doing both at once.
What a Closure Actually Captures
A closure is created when a nested function references a variable from its enclosing function, and the enclosing function returns that nested function. The nested function retains access to those variables even after the enclosing function has finished executing.
def make_multiplier(factor): def multiply(value): return value * factor return multiply double = make_multiplier(2) print(double(5)) # 10
Here multiply captures factor from make_multiplier's scope. When make_multiplier returns, factor is not discarded because multiply still references it. The captured cell survives as long as the returned function does.
The captured variable is read at call time, not at creation time. If the enclosing function modifies the variable after returning the nested function, the nested function sees the modified value. This behavior matters when you build stateful closures.
def counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment next_value = counter() print(next_value()) # 1 print(next_value()) # 2
The nonlocal declaration is required because increment assigns to count rather than merely reading it. Without nonlocal, Python would treat count as a local variable inside increment and raise an UnboundLocalError on the first call.
What a Decorator Adds on Top
A decorator is a function that takes a callable and returns a callable, usually replacing the original with a wrapped version. The @ syntax is syntactic sugar for assigning the result back to the original name.
def log_calls(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper @log_calls def add(a, b): return a + b print(add(2, 3))
The decorator log_calls receives add as func. The inner wrapper is a closure because it captures func from the enclosing scope. When you call add(2, 3), you are actually calling wrapper, which logs the call and then invokes the original add.
The closure is the mechanism; the decorator is the pattern that uses that mechanism. Every decorator that wraps a function with an inner function is creating a closure. But not every closure is a decorator. A closure becomes a decorator only when it is applied to another callable through the @ syntax or an equivalent assignment.
The Relationship Between the Two
The clearest way to state the relationship is that decorators are a user-facing language feature, while closures are a runtime behavior of nested functions. You can write a decorator without the @ syntax by doing the assignment manually:
def add(a, b): return a + b add = log_calls(add)
This is exactly what the @ syntax does under the hood. The decorator function is still relying on the closure created by wrapper to keep func alive.
Closures appear in many places that have nothing to do with decorators. Callback functions, factory functions, and partial application all use closures. Decorators are a specific application of closures that targets function or class wrapping.
Key Differences at a Glance
| Aspect | Closure | Decorator |
|---|---|---|
| Core purpose | Captures state from an enclosing scope | Wraps or modifies a callable |
| Created by | Nested function referencing outer variables | Applying a wrapper function to a callable |
| Syntax | Plain nested function | @decorator or manual assignment |
| Always a closure? | Yes, by definition | No, a decorator may return the original callable |
| Always a decorator? | No | Yes, by definition |
A decorator does not have to return a closure. It can return the original function unchanged, or it can return a different callable that does not capture anything. In practice, most useful decorators return a wrapper, which makes them closures as well.
When to Use Each
Write a closure directly when you need a function that carries configuration or state with it. A factory function that produces configured callbacks is a natural fit.
Write a decorator when you want to apply the same transformation to many functions without repeating the wrapping logic at each call site. Logging, timing, retry logic, and access control are common decorator use cases.
def retry(max_attempts): def decorator(func): def wrapper(*args, **kwargs): for attempt in range(max_attempts): try: return func(*args, **kwargs) except Exception: if attempt == max_attempts - 1: raise return None return wrapper return decorator
This example shows both concepts working together. retry is a decorator factory. decorator is the actual decorator. wrapper is a closure that captures both func and max_attempts. The nested structure is common when a decorator needs its own arguments.
Runtime Cost and Metadata Considerations
Every closure adds a small indirection when the wrapped function is called. The wrapper function call, the argument forwarding through *args and **kwargs, and the captured cell lookups all add overhead compared to calling the original function directly. For most application code this cost is negligible, but for hot paths inside tight loops it can be measurable.
A more visible problem is metadata loss. The wrapper replaces the original function, so attributes like __name__, __doc__, and __annotations__ point at the wrapper, not the original function. The functools.wraps helper copies these attributes onto the wrapper.
from functools import wraps def log_calls(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper
Without @wraps, tooling that inspects function signatures, such as debuggers or documentation generators, will report the wrapper instead of the decorated function. The closure still works correctly, but the metadata is wrong.
Common Failure Modes
The most frequent closure bug is forgetting nonlocal when a nested function assigns to a captured variable. The second most common issue is capturing a loop variable by reference rather than by value.
funcs = [] for i in range(3): funcs.append(lambda: i) print([f() for f in funcs]) # [2, 2, 2]
All three lambdas capture the same i cell, which holds the final loop value. The fix is to bind the value as a default argument or create a new scope per iteration. The same problem appears in decorator factories that capture a mutable default value.
For decorators specifically, a common mistake is returning the original function instead of a wrapper when the decorator is supposed to modify behavior. If the decorator only registers the function somewhere and returns it unchanged, that is valid. If it is supposed to change behavior but returns the original, the decoration silently does nothing.
Choosing Between a Decorator and a Plain Closure
The decision is rarely about which is better. It is about what the code is trying to express. If the goal is to transform or augment a specific callable, use a decorator. If the goal is to produce a callable that carries state, use a closure directly.
A decorator that does not need arguments can be written as a single-level function. A decorator that needs arguments requires a factory, which adds one more level of nesting. When the extra nesting makes the code harder to read, consider whether a plain closure or a small helper class would be clearer.
Closures and decorators are both higher-order function techniques. Understanding the closure mechanism makes decorator code easier to debug, because the captured variables and the wrapper call flow become explicit rather than magical.