Decorators and Closures
Source: Fluent Python, 2nd Edition — Chapter 9 by Luciano Ramalho
A decorator is a callable that takes a function and returns a function (often a wrapper). The @ line is sugar:
@deco
def target():
...
# same as: target = deco(target)
Decorators run at import time — as soon as the def finishes — not when you later call the function. Closures (and nonlocal) are how wrappers remember the original function and extra state.
This chapter covers:
- What
@really does, and registration vs wrapping - Local vs global names (
UnboundLocalError) - Closures and free variables
nonlocalwhen you assign to a captured namefunctools.wraps,@cache/@lru_cache,@singledispatch- Decorator factories (decorators that take arguments)
Decorators 101
The decorator may return the same function (registration) or a new inner function that calls the original.
Three facts: a decorator is a callable; it may replace the decorated name; it runs when the module loads.
🧪 Replacement at decoration time
Mini quiz — sugar and import time
Decorators 101
1 / 2@deco above def f(): is the same as
Pick an answer to see if you are right.
Challenge: plugin registry
Build a list plugins and a decorator plugin that appends the function and returns it unchanged. Calling the decorated function must still run the original body.
Variable scope
Python decides at compile time whether a name in a function is local: if you assign to it anywhere in the body, it is local for the whole function. Reading it before that assignment raises UnboundLocalError, even if a global with the same name exists.
global x means assign/read the module-level x. Nested functions that assign to a name from the enclosing function need nonlocal x.
🧪 Assignment makes it local
Mini quiz — locals
Variable scope
1 / 2A name assigned anywhere in a function is treated as
Pick an answer to see if you are right.
Closures
A closure is a nested function that keeps free variables from the enclosing scope after that scope has returned. Mutating a list (append) does not rebind the name. Rebinding a number (count += 1) does assign, so you need nonlocal count.
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
return sum(series) / len(series)
return averager
🧪 Running average via closure
Mini quiz — closures and nonlocal
Closures
1 / 2A free variable is
Pick an answer to see if you are right.
Challenge: running mean
Return a function that, given a stream of numbers, returns the mean of all values so far.
A wrapping decorator
Typical wrapper: accept *args, **kwargs, call the original, maybe log, return its result. Copy __name__ / __doc__ with @functools.wraps(func).
Stacked @a @b def f means f = a(b(f)) — the closest decorator runs first.
🧪 wraps keeps the name
Challenge: shout wrapper
Write shout so @shout makes the function’s return value uppercase. The original still receives the same argument.
Library helpers
| Tool | Role |
|---|---|
functools.cache (3.9+) | Unbounded memoization. Arguments must be hashable. Fine for short scripts. |
functools.lru_cache(maxsize=128) | Same idea with a size cap. Prefer this in long-running services. |
functools.singledispatch | Pick an implementation from the type of the first argument. Register more types in other modules. Prefer ABCs over only int. |
Naive recursive Fibonacci without a cache repeats work; @cache (or @lru_cache) keeps one result per n. @cache can grow without bound — use lru_cache when that matters.
from functools import singledispatch
@singledispatch
def show(obj):
return repr(obj)
@show.register
def _(text: str):
return text.upper()
Mini quiz — stdlib
cache and singledispatch
1 / 2@alpha @beta def f means
Pick an answer to see if you are right.
Parameterized decorators
@clock receives the function. @clock(fmt) must be a factory: clock(fmt) returns the real decorator, which then receives the function.
A class with __init__(self, fmt) and __call__(self, func) is the same idea and often easier to read for non-trivial wrappers.
If the factory takes optional args, write @register() with parentheses when you want the default — otherwise Python would pass the function as that first parameter.
Mini quiz — factories
Parameterized decorators
1 / 2@repeat(3) def f needs repeat to
Pick an answer to see if you are right.
Challenge: call n times
times(n) is a decorator factory. The wrapped function should be called n times on the same int, and those return values summed.
Chapter summary
@decomeansf = deco(f)and runs at import time.- Registration keeps the same function; most decorators return a wrapper.
- Assignment anywhere in a function makes that name local.
- Closures keep enclosing bindings;
nonlocalis required to rebind them. - Use
wraps,cache/lru_cache, andsingledispatchfromfunctools. - Parameterized
@deco(x)is a factory that returns the real decorator.
Exercises
make_counter() returns a function. Each call with no arguments adds 1 to a hidden count and returns the new count. Two counters from two factory calls must not share state.