Skip to main content

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
  • nonlocal when you assign to a captured name
  • functools.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​

Quiz

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​

Quiz

Variable scope

1 / 2

A 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​

Quiz

Closures

1 / 2

A 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​

ToolRole
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.singledispatchPick 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​

Quiz

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​

Quiz

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​

Key takeaways
  1. @deco means f = deco(f) and runs at import time.
  2. Registration keeps the same function; most decorators return a wrapper.
  3. Assignment anywhere in a function makes that name local.
  4. Closures keep enclosing bindings; nonlocal is required to rebind them.
  5. Use wraps, cache / lru_cache, and singledispatch from functools.
  6. Parameterized @deco(x) is a factory that returns the real decorator.

Exercises​

Activity: Nonlocal counter

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.