Closures and Decorators
Closures capture enclosing variables; decorators wrap functions. Covers the late-binding-in-loops bug and correct use of @functools.wraps.
A closure is a function that remembers variables from the scope where it was defined, even after that scope has finished executing. A decorator is a higher-order function - a function that takes another function and returns a (usually wrapped) replacement. Python's @decorator syntax is shorthand for fn = decorator(fn).
When a function is defined inside another function, it has access to the outer function's local variables. Each call to the outer function creates its own independent set of those variables, so closures built from separate calls are isolated from each other.
def multiplier(n):
def multiply(x):
return x * n # n is captured from the enclosing scope
return multiply
double = multiplier(2)
triple = multiplier(3)
double(5) # 10
triple(5) # 15
# Each closure has its own n; they don't share state
double(10) # 20
triple(10) # 30Closures capture the variable, not the value at the time of capture. When a loop builds closures that all reference the same loop variable, every closure sees the variable's final value - not the value it held during that iteration. This is called late binding and is a common source of bugs. The fix is to force an early bind by using a default argument or functools.partial.
# Bug: all three lambdas capture the same variable i
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] -- all see i=2 (final value)
# Fix 1: bind eagerly with a default argument
funcs_fixed = [lambda i=i: i for i in range(3)]
[f() for f in funcs_fixed] # [0, 1, 2]
# Fix 2: use functools.partial
import functools
def identity(x):
return x
funcs_partial = [functools.partial(identity, i) for i in range(3)]
[f() for f in funcs_partial] # [0, 1, 2]A decorator is a function that wraps another function to add behaviour before or after the call. Always apply @functools.wraps(fn) to the inner wrapper: without it the decorated function loses its __name__, __doc__, and signature, which breaks introspection tools, API frameworks like FastAPI, and test helpers that inspect function metadata.
import functools
import time
def timed(fn):
@functools.wraps(fn) # copies __name__, __doc__, __annotations__ from fn
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{fn.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timed
def slow_add(a, b):
"""Add two numbers slowly."""
time.sleep(0.01)
return a + b
slow_add(2, 3)
# slow_add took 0.0101s
# 5
# Without @functools.wraps these would return "wrapper" and None:
slow_add.__name__ # "slow_add"
slow_add.__doc__ # "Add two numbers slowly."In production
Closures capture variables, not values - [lambda: i for i in range(3)] produces three lambdas that all return 2; bind eagerly with lambda i=i: i or restructure with functools.partial. Always use @functools.wraps(fn) inside a decorator - without it the wrapped function loses __name__, __doc__, and signature, breaking every tool that introspects (Sphinx docs, FastAPI route discovery, pytest fixture injection). Stacking multiple decorators is fine; the one closest to the def line applies first.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free