Decorators and Closures
Notes based on Fluent Python, summarized and rephrased in my own words.[file:21]
Why decorators need closures
You can use Python happily without ever writing your own decorators, but as soon as you do, you need to understand closures and the nonlocal statement.[file:21] Closures are also an essential tool for callback-style asynchronous code and for a functional programming style.[file:21]
At a high level:
- A decorator is a callable that takes a function and returns another callable (often wrapping the original function).[file:21]
- A closure is a function plus the references to free variables captured from the surrounding scope.[file:21]
Decorators are usually implemented by writing an inner function that closes over some variables, then returning that inner function.
Decorator basics
The decorator syntax:
@decorate
def target():
print('running target()')is equivalent to:
def target():
print('running target()')
target = decorate(target)So @decorate is just syntactic sugar that transforms the function object after it is defined.[file:21]
A typical decorator replaces the original function with a new function:
def deco(func):
def inner():
print('running inner()')
return inner
@deco
def target():
print('running target()')
# target now refers to innerCalling target() actually runs inner(), not the original body.[file:21]
Decorators are very useful for metaprogramming, i.e. changing behavior at import or runtime without modifying the original function body.[file:21]
When Python executes decorators
A crucial detail: decorators run right after the decorated function is defined—typically at import time.[file:21]
For example, consider a simple registry:
registry = []
def register(func):
print('running register(%s)' % func)
registry.append(func)
return func
@register
def f1():
print('running f1()')
@register
def f2():
print('running f2()')
def f3():
print('running f3()')When the module is imported, Python executes the top-level code, which includes the decorator applications. That means register(f1) and register(f2) are called immediately, populating the registry list even before main() runs.[file:21]
This "run at import" behavior is why decorators are often used for:
- Automatically registering functions in a plugin system.[file:21]
- Connecting handler functions to events or routes.
Variable scope rules and the UnboundLocalError
Understanding closures also requires a clear picture of how Python decides whether a name is local, nonlocal, or global.
If a variable is assigned anywhere inside a function body, Python treats it as a local variable in that function.[file:21] This is true even if there is a global variable with the same name.
b = 6
def f2(a):
print(a)
print(b) # error: b is considered local because of the assignment below
b = 9
f2(3)This raises UnboundLocalError because Python decides b is local to f2 (due to b = 9), but you try to read b before that assignment.[file:21]
To modify a global variable inside a function, you must use global b. For variables in an enclosing (non-global) scope, you use nonlocal (see below).[file:21]
Closures
A closure is a function that remembers the values of free variables from the environment where it was created, even after that environment is gone.[file:21]
Example: a running average calculator built with a closure:
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total / len(series)
return averager
avg = make_averager()
avg(10) # 10.0
avg(11) # 10.5
avg(12) # 11.0Here:
seriesis a local variable inmake_averager.averageris an inner function that usesseries, but does not assign to it.- After
make_averagerreturns, its local scope is gone, butavgstill has access toseriesthrough the closure.[file:21]
Technically:
- Variables like
seriesthat are not local toaveragerbut are referenced inside it are called free variables.[file:21] - The function object
avgcarries a__closure__attribute that stores cell objects referencing those free variables.[file:21]
In short: a closure is a function plus the bindings of its free variables.
The nonlocal declaration
The previous make_averager stored all historical values in a list and recomputed the sum each time. A more efficient version would only store the count and the running total:
def make_averager():
count = 0
total = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averagerThis version fails with UnboundLocalError, because count += 1 and total += new_value are treated as assignments to local variables inside averager, hiding the bindings from the outer scope.[file:21]
To fix this, we declare the variables as nonlocal inside the inner function:
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averagerKey points:
nonlocaltells Python: "this variable lives in an enclosing function scope, not locally".[file:21]- Assignments then update the binding stored in the closure instead of creating a new local variable.[file:21]
- This is essential when you need to update state stored in a closure, not just read it.
Implementing a simple decorator
A basic timing decorator can be written using a closure:
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clockedHere:
clockis the decorator.clockedis the inner wrapper function, which:- Records the start time.
- Calls the original
func. - Logs timing information.
- Returns the result.[file:21]
clockreturnsclocked, which closes overfunc.[file:21]
Using it:
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)Whenever we call snooze or factorial, we’re actually calling the clocked wrapper, which then calls the original function.[file:21]
Limitations of the first version:
- It does not support keyword arguments.
- It hides the original function’s metadata (
__name__,__doc__).[file:21]
Preserving metadata with functools.wraps
To fix these issues, we write a more robust version using functools.wraps and support for **kwargs:
import time
import functools
def clock(func):
@functools.wraps(func)
def clocked(*args, **kwargs):
t0 = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - t0
name = func.__name__
arg_list = []
if args:
arg_list.append(', '.join(repr(a) for a in args))
if kwargs:
pairs = [f"{k}={v!r}" for k, v in sorted(kwargs.items())]
arg_list.append(', '.join(pairs))
arg_str = ', '.join(arg_list)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked@functools.wraps(func) copies important metadata (__name__, __doc__, and others) from func to clocked, so tools like debuggers and help() show the wrapped function correctly.[file:21]
Useful standard-library decorators
Python’s standard library ships with many decorators that embody common patterns:[file:21]
@functools.lru_cache(maxsize=128, typed=False)- Caches results of expensive pure functions to speed up repeated calls.
maxsizelimits cache memory;typed=Truetreats different types as distinct keys.
@functools.wraps(wrapped)- Helps you write decorators by copying metadata from the wrapped function to the wrapper.
@functools.total_ordering- Given one or two rich comparison methods on a class, it fills in the rest.
@functools.singledispatch- Turns a function into a single-dispatch generic function that selects an implementation based on the type of the first argument.[file:21]
@property,@staticmethod,@classmethod- Control how methods are accessed on a class; they are all built as decorators.[file:21]
@abc.abstractmethod- Marks methods in abstract base classes that must be overridden in subclasses.[file:21]
@contextlib.contextmanager- Lets you write context managers (
withblocks) using a simple generator-style function instead of a full class.[file:21]
- Lets you write context managers (
Example: @functools.singledispatch
singledispatch implements generic functions that choose behavior based on the first argument’s type:[file:21]
from functools import singledispatch
@singledispatch
def show(arg, verbose=False):
if verbose:
print('Default case:', end=' ')
print(arg)
@show.register(int)
def _(arg, verbose=False):
if verbose:
print('Handling an int:', end=' ')
print(arg)
@show.register(list)
def _(arg, verbose=False):
if verbose:
print('Handling a list:')
for i, elem in enumerate(arg):
print(i, elem)- The base
@singledispatch-decorated function defines the default behavior.[file:21] @show.register(int)and@show.register(list)add specialized implementations for specific types.[file:21]- If no specialized implementation matches, the base function is used.[file:21]
This is much cleaner than a long if isinstance(...) chain.
Stacking (composing) decorators
Multiple decorators can be applied to the same function:
@d1
@d2
def f():
print('f')This is equivalent to:
def f():
print('f')
f = d1(d2(f))So the decoration order is from the bottom up: d2 wraps f, then d1 wraps the result.[file:21]
This lets you build complex behavior by composing simple, reusable decorators.
Parameterized decorators
Sometimes you want the decorator itself to accept arguments. Python’s decorator syntax only passes the function, so we need a factory that returns a real decorator.
Conceptually:
def outer(arg):
def decorator(func):
# use arg and func
return wrapper
return decorator
@outer(config_value)
def target():
...A concrete example is a registry decorator that can be enabled or disabled:[file:21]
registry = set()
def register(active=True):
def decorate(func):
print(f'running register(active={active})->decorate({func})')
if active:
registry.add(func)
else:
registry.discard(func)
return func
return decorate
@register(active=False)
def f1():
print('running f1()')
@register() # active defaults to True
def f2():
print('running f2()')
def f3():
print('running f3()')Here:
register(active=...)is called at import time and returnsdecorate.decorateis then applied tof1/f2as the actual decorator.[file:21]- The decorator has access to both
activeandfuncthrough a closure.
This pattern is the standard way to write decorators that take their own configuration arguments.