The same few lines of logging or timing belong in many functions, and none of those functions is about logging or timing. A decorator wraps that behavior around a function from outside.
Logging, timing, retrying, and validating arguments are cross-cutting concerns: they show up in unrelated functions, not just one. Writing them into each function’s own body spreads the same few lines everywhere they apply, and a change to that logic means editing every copy.
A decorator is a callable that you apply to a function or a class. The decorator receives the thing it decorates, does something with it, then returns a result, which Python binds to the original name. Most decorators apply to functions.
@ DoesTo apply a decorator, put @ followed by the
decorator name on the line above the definition.
# simple_decoration.py
from collections.abc import Callable
def hijack(func: Callable) -> Callable:
def doesnt_matter() -> None:
print("Replacement behavior")
return doesnt_matter
@hijack
def cheese() -> None:
print("Wensleydale")
cheese()
#: Replacement behaviorThe @hijack above cheese()
means:
cheese = hijack(cheese)
The @ line is syntactic sugar for that
assignment: the same call and the same rebinding, written above
the function instead of after it. hijack returns
doesnt_matter, which Python assigns to the name
cheese, so cheese now refers to
doesnt_matter. Calling cheese() runs
doesnt_matter, which ignores func and
prints its own message instead. The original body of
cheese() never runs.
Since Python binds the returned function to the name
cheese, the local name
(doesnt_matter()) can be anything. Convention calls
it wrapper().
A typical decorator returns a wrapper that does some work, calls the original function, then does some more work:
# typical_decorator.py
from collections.abc import Callable
def add_behavior(func: Callable) -> Callable:
def wrapper() -> None:
print("Some work")
func()
print("Some more work")
return wrapper
@add_behavior
def cheese() -> None:
print("Wensleydale")
cheese()
#: Some work
#: Wensleydale
#: Some more workA decorator that forgets its return wrapper
returns None instead, so Python binds
cheese to None. The failure surfaces
at the next call to cheese(), as a
TypeError reporting that a NoneType
object is not callable, not at the decoration that caused
it.
The decorator runs when Python executes the def,
not when you call the decorated function:
# decoration_time.py
from collections.abc import Callable
def announce(func: Callable) -> Callable:
print("Decorating")
def wrapper() -> None:
print("Calling")
func()
return wrapper
@announce
def cheese() -> None:
print("Wensleydale")
#: Decorating
print("Definitions done")
#: Definitions done
cheese()
#: Calling
#: WensleydaleDecorating prints before
Definitions done, so announce runs
when Python executes the def for
cheese(), before any call. Only the body of
wrapper() waits for the call.
wrapper() is a closure.
Defined inside its decorator, it refers to func, a
variable from the enclosing scope, not one of its own
parameters. Python keeps func alive for as long as
wrapper() exists, even after the decorator has
returned. That lets cheese(), called long after
decoration finished, still reach the original
cheese function through func.
Decoration is a simple kind of metaprogramming. The same idea appears in design patterns as the Decorator pattern: wrap an object to add responsibilities to it, while keeping the wrapped object’s interface so the wrapping stays invisible to the code that uses it.
The wrappers so far declare no parameters, so
add_behavior is limited to functions that take
none. With add_behavior on a
def add(a, b), the call add(2, 3)
raises a TypeError. The wrapper takes zero
positional arguments, and the call passes two. A wrapper that
must handle any function collects the call with
*args, **kwargs and forwards it unchanged, the argument-unpacking
pattern. This decorator traces calls, and its
wrapper() takes that shape:
# tracer.py
from collections.abc import Callable
from functools import wraps
def trace[**P, R](func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
positional = [repr(a) for a in args]
named = [f"{k}={v!r}" for k, v in kwargs.items()]
arglist = ", ".join(positional + named)
print(f"-> {func.__name__}({arglist})") # type: ignore
result = func(*args, **kwargs)
print(f"<- {func.__name__} = {result!r}") # type: ignore
return result
return wrapper
@trace
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
add(2, b=3)
#: -> add(2, b=3)
#: <- add = 5wraps
Keeps the Runtime Interfacefunctools.wraps copies the original function’s
metadata onto the wrapper: __module__,
__name__, __qualname__,
__doc__, __annotate__, and
__type_params__, the tuple
functools.WRAPPER_ASSIGNMENTS, and it merges the
original’s __dict__ into the wrapper’s. Without it,
the decorated add reports its name as
wrapper and loses its docstring, misleading
debuggers, help(), and documentation tools.
wraps is optional, and reasons to omit it are rare.
wraps also sets __wrapped__ on the
wrapper, pointing at the original, so
add.__wrapped__(2, 3) calls the function without
the tracing, and inspect.signature() follows that
chain automatically.
The tests verify two things: the wrapper reports the original function’s name, and it still returns the original result:
# test_tracer.py
from tracer import trace
def test_trace_preserves_name() -> None:
@trace
def add(a: int, b: int) -> int:
return a + b
assert add.__name__ == "add"
def test_trace_returns_original_result() -> None:
@trace
def add(a: int, b: int) -> int:
return a + b
assert add(2, 3) == 5**P and
R Keep the Static Interfacewraps keeps the runtime interface, and the type
parameters keep the static one. trace[**P, R]
declares two of them. R is the wrapped function’s
return type. **P is a parameter
specification (ParamSpec). It captures the
whole parameter list of the wrapped function as a single unit,
names and types included. func: Callable[P, R]
reads as “a function whose parameters are P and
whose result is R,” and returning
Callable[P, R] declares that the wrapper has that
same signature.
Inside the wrapper, *args: P.args and
**kwargs: P.kwargs are the two halves of that
captured list. P.args is the positional part and
P.kwargs the keyword part. They must appear
together, as the *args and **kwargs of
a function typed with P. They bind the wrapper’s
arguments to the parameters captured by **P, so the
type checker accepts add(2, 3) but rejects
add("x") or add(2, 3, 4), although the
body of wrapper() forwards anything. Without
**P you fall back to
*args: Any, **kwargs: Any, and the wrapper accepts
any arguments, discarding the signature the decorator should
preserve.
R checks the wrapper’s result the same way. A
wrapper that calls func(*args, **kwargs) without
returning the result makes every decorated function return
None, and the -> R annotation lets
the type checker report the missing return.
The # type: ignore comments silence a diagnostic
from ty, which rejects func.__name__.
A Callable need not have a __name__
attribute, though every function does. Pyright and mypy both
accept the attribute here.
async Wrappertrace assumes func runs to
completion inside the call that invokes it, which is true of an
ordinary function and false of an async def
function. Decorating a coroutine function raises no exception.
func(*args, **kwargs) returns a coroutine object
immediately, without running the coroutine’s body, so
result holds that coroutine object rather than the
value the coroutine will eventually produce. The trace line then
prints
<- add = <coroutine object add at 0x...>.
A wrapper over a coroutine function must be async def
and await func(*args, **kwargs). The annotations
change to match: func is a
Callable[P, Awaitable[R]], the wrapper is
async def wrapper(...) -> R, and the decorator
returns a Callable[P, Coroutine[Any, Any, R]],
since calling an async def function produces a
coroutine rather than an R.
To pass arguments to a decorator, add another layer. A decorator with arguments is a function that returns a decorator:
# repeat.py
from collections.abc import Callable
from functools import wraps
def repeat[**P, R](
times: int
) -> Callable[[Callable[P, R]], Callable[P, R]]:
if times < 1:
raise ValueError(f"times must be >= 1, got {times}")
def decorate(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorate
@repeat(times=3)
def greet(name: str) -> str:
print(f"Hello, {name}")
return name
if __name__ == "__main__":
greet("Bob")
#: Hello, Bob
#: Hello, Bob
#: Hello, BobTake the return type apart:
Callable[[Callable[P, R]], Callable[P, R]]. Callable[[A, B], X]
reads as “a callable that takes A and
B and returns X”. The first bracket
group is a list holding the parameter types, so
[Callable[P, R]] is a parameter list of length one,
not a list of callables. That single parameter type is
Callable[P, R], the wrapped function’s type. So the
whole annotation reads as “a callable that takes a
Callable[P, R] and returns a
Callable[P, R].” The annotation describes
decorate, which takes func and returns
wrapper, both typed
Callable[P, R].
@repeat(times=3) first evaluates
repeat(times=3). repeat(times=3)
returns decorate, the real decorator, and Python
then calls decorate(greet). Only now does
decorate’s own body run, including its
@wraps(func) line. @wraps(func) is the
same two-step pattern one level down. Python calls
wraps(func) to get a decorator, then applies that
decorator to wrapper. The inner decoration runs
inside the outer one, so @repeat(times=3) and
@wraps(func) are two separate applications of the
same two-step pattern, not one recursive call. The nesting stops
at two levels, matching the two nested defs in the
source.
Forgetting the parentheses is the common mistake here.
@repeat without them calls
repeat(greet), passing the function where
repeat expects times. The
times < 1 check turns that mistake into a
TypeError at decoration, since a function does not
support < 1, though the message says nothing
about parentheses. A repeat without that comparison
fails silently. Python binds greet to
decorate. Calling greet("Bob") then
passes "Bob" where decorate expects a
function and hands back a wrapper, and the only symptom is
missing output.
The annotations catch the mistake either way, at the
decoration rather than at the call. The type checker reports
that repeat expected an int for
times and got a function. A second diagnostic
follows at greet("Bob"), but that one is harder to
read back to the missing ().
repeat() rejects times below one
rather than quietly rounding it up to one. The check runs at
decoration, so the failure appears at the @ line
rather than at some later call. Because that check guarantees
times >= 1, wrapper()’s loop always
runs at least once, so result always holds a value
of type R to return. test_repeat.py
parametrizes over times and covers the
rejection:
# test_repeat.py
import pytest
from repeat import repeat
@pytest.mark.parametrize("times, expected", [
(3, 3),
(1, 1),
])
def test_repeat_call_count(times: int,
expected: int) -> None:
calls: list[str] = []
@repeat(times=times)
def record() -> None:
calls.append("call")
record()
assert len(calls) == expected
@pytest.mark.parametrize("times", [0, -1])
def test_repeat_rejects_times_below_one(times: int) -> None:
with pytest.raises(ValueError):
repeat(times=times)trace takes no arguments, but
repeat always does. A decorator can support both
conventions at once, @name and
@name(...), letting a caller add arguments only
when the defaults do not fit. pytest.fixture and
click.command both work this way.
The two forms differ in what Python passes first.
@label calls label(one), so a function
arrives as the only argument. @label(prefix="TAG")
calls label(prefix="TAG") first, with no function,
then applies the result to two. Checking whether
that first argument is callable tells the two calls apart:
# optional_parens.py
from collections.abc import Callable
from functools import wraps
from typing import Any, overload
type Decorator[**P, R] = Callable[
[Callable[P, R]], Callable[P, R]]
@overload
def label[**P, R](
func: Callable[P, R],
) -> Callable[P, R]: ...
@overload
def label[**P, R](
func: None = None, *, prefix: str = "LOG"
) -> Decorator[P, R]: ...
def label[**P, R](
func: Callable[P, R] | None = None,
*, prefix: str = "LOG",
) -> Any:
def decorate(
f: Callable[P, R]
) -> Callable[P, R]:
@wraps(f)
def wrapper(
*args: P.args, **kwargs: P.kwargs
) -> R:
print(f"[{prefix}] {f.__name__}") # type: ignore
return f(*args, **kwargs)
return wrapper
return decorate(func) if callable(func) else decorate
@label
def one() -> None: ...
@label(prefix="TAG")
def two() -> None: ...
if __name__ == "__main__":
one()
two()
#: [LOG] one
#: [TAG] twoThe second overload returns the shape repeat.py takes apart, now
under the name Decorator[P, R]: a generic alias
whose parameters are the wrapped function’s. func
defaults to None, and the body branches on
callable(func). Called bare, func is
one, callable(func) is
True, so label decorates it
immediately by calling decorate(func). Called with
arguments, func stays None,
callable(func) is False, so
label returns decorate for Python to
apply to two.
The two @overload declarations state for the
type checker what the runtime branch does: given a function,
label returns a function of the same signature;
given only keyword arguments, it returns a decorator. The
implementation must satisfy both overloads, so it declares the
widest return type, Any, and the overloads narrow
that back down at every call site.
The callable(func) test assumes that only the
decorated function can arrive in that first position. Where a
decorator’s own argument could be callable, checking
func is None instead of callable(func)
removes the ambiguity.
The tests decorate one function each way and confirm that both forms keep the result and the name:
# test_optional_parens.py
from optional_parens import label
def test_bare_decoration() -> None:
@label
def greet() -> str:
return "hi"
assert greet() == "hi"
assert greet.__name__ == "greet"
def test_decoration_with_arguments() -> None:
@label(prefix="TAG")
def greet() -> str:
return "hi"
assert greet() == "hi"
assert greet.__name__ == "greet"A decorator can wrap the wrapper another decorator produced. Decorators stack, nesting from the bottom up:
# stacking.py
from repeat import repeat
from tracer import trace
@trace
@repeat(times=2)
def greet(name: str) -> str:
print(f"Hello, {name}")
return name
if __name__ == "__main__":
greet("Bob")
#: -> greet('Bob')
#: Hello, Bob
#: Hello, Bob
#: <- greet = 'Bob'The two @ lines mean
greet = trace(repeat(times=2)(greet)).
@repeat(times=2) wraps greet() first,
then @trace wraps that result, so a single
greet("Bob") traces one call whose body runs
twice.
Stacking works because each wrapper preserves the interface of what it wraps. Every layer looks like the original function, so the layers compose to any depth.
The tests confirm that the name survives two layers of wrapping, and that one call through both layers still runs the body twice:
# test_stacking.py
from repeat import repeat
from tracer import trace
def test_stacked_decorators_preserve_name() -> None:
@trace
@repeat(times=2)
def greet(name: str) -> str:
return name
assert greet.__name__ == "greet"
def test_stacked_decorators_repeat_the_call() -> None:
calls: list[str] = []
@trace
@repeat(times=2)
def record() -> None:
calls.append("call")
record()
assert calls == ["call", "call"]A decorator is any callable that accepts one argument. A
class with __call__() is a callable, so a decorator
can be a class instead of a function.
The class form separates the two phases cleanly. The
constructor runs once, at decoration, and
__call__() runs on every call to the decorated
function.
These classes take lowercase names, against the usual
PascalCase rule, because a decorator reads like a
function at the call site. property,
staticmethod, and functools.partial
are all lowercase classes for that reason.
The class version of trace:
# trace_class.py
from collections.abc import Callable
from functools import update_wrapper
class trace[**P, R]:
__name__: str # Set by update_wrapper(), not __init__
def __init__(self, func: Callable[P, R]) -> None:
self.func = func
# Copy __name__, __doc__, etc
update_wrapper(self, func)
def __call__(self, *args: P.args,
**kwargs: P.kwargs) -> R:
positional = [repr(a) for a in args]
named = [f"{k}={v!r}" for k, v in kwargs.items()]
arglist = ", ".join(positional + named)
print(f"-> {self.func.__name__}({arglist})") # type: ignore
result = self.func(*args, **kwargs)
print(f"<- {self.func.__name__} = {result!r}") # type: ignore
return result
@trace
def add(a: int, b: int) -> int:
return a + b
if __name__ == "__main__":
add(2, b=3)
#: -> add(2, b=3)
#: <- add = 5@trace runs add = trace(add), so
the constructor receives the function and stores it. The name
add now refers to a trace instance,
and calling add(2, 3) invokes
__call__().
functools.update_wrapper(self, func) copies
func’s metadata onto an existing object.
wraps is the decorator form of that same call.
@wraps(func) above def wrapper runs
update_wrapper(wrapper, func). The class form has
no inner function to decorate, only self, so its
__init__() calls
update_wrapper(self, func).
Like the function form, the class is generic in
**P and R, so __call__()
keeps the wrapped signature and add(2, 3) still
type-checks as an int.
The class form passes the function form’s two tests,
test_trace_preserves_name and
test_trace_returns_original_result, with only the
import changed:
# test_trace_class.py
from trace_class import trace
def test_trace_preserves_name() -> None:
@trace
def add(a: int, b: int) -> int:
return a + b
assert add.__name__ == "add"
def test_trace_returns_original_result() -> None:
@trace
def add(a: int, b: int) -> int:
return a + b
assert add(2, 3) == 5Because the instance can hold attributes, it can carry state between calls. This decorator counts calls and keeps the count on the instance:
# count_calls.py
from collections.abc import Callable
from functools import update_wrapper
class count_calls[**P, R]:
def __init__(self, func: Callable[P, R]) -> None:
self.func = func
self.count = 0
update_wrapper(self, func)
def __call__(self, *args: P.args,
**kwargs: P.kwargs) -> R:
self.count += 1
print(f"call {self.count} of {self.func.__name__}") # type: ignore
return self.func(*args, **kwargs)
@count_calls
def hello() -> None:
print("hello")
if __name__ == "__main__":
hello()
hello()
# The state lives on the decorator instance
print(hello.count)
#: call 1 of hello
#: hello
#: call 2 of hello
#: hello
#: 2Each @count_calls creates its own instance, so
each decorated function keeps its own count:
# test_count_calls.py
from count_calls import count_calls
def test_counts_are_independent_per_function() -> None:
@count_calls
def greet() -> None:
pass
@count_calls
def farewell() -> None:
pass
greet()
greet()
farewell()
assert greet.count == 2
assert farewell.count == 1The function form can keep a count too, in a variable of the
decorator’s body that wrapper() updates through nonlocal.
That variable is visible only inside the closure, while
hello.count is an attribute any caller can read.
The wrapper’s own __dict__ could hold the count as
well, but the type checker reports every dotted access to an
attribute stored there, so call state goes in a closure or an
instance. Attributes
on a Function shows what a function’s __dict__
does suit.
The class form pays off when the decorator takes arguments.
Without arguments, the constructor receives the function. With
arguments, the constructor receives the arguments, and
__call__() receives the function and returns the
wrapper:
# repeat_class.py
from collections.abc import Callable
from functools import wraps
class repeat:
def __init__(self, times: int) -> None:
if times < 1:
raise ValueError(
f"times must be >= 1, got {times}")
self.times = times # The decoration arguments
def __call__[**P, R](
self, func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
result = func(*args, **kwargs)
for _ in range(self.times - 1):
result = func(*args, **kwargs)
return result
return wrapper
@repeat(times=3)
def greet(name: str) -> None:
print(f"Hello, {name}")
if __name__ == "__main__":
greet("Bob")
#: Hello, Bob
#: Hello, Bob
#: Hello, Bobrepeat here validates times the
same way repeat.py does, in the
constructor rather than in the outer function. Its
wrapper() makes the first call before the loop, so
result has a value whether or not the loop runs,
where repeat.py relies on the
times >= 1 check for that. With decorator
arguments, the class form is typically easier to reason about
than the function
form.
test_repeat_class.py
repeats test_repeat.py with only
the import changed: test_repeat_call_count
parametrizes over times, and
test_repeat_rejects_times_below_one covers the
rejection in the constructor:
# test_repeat_class.py
import pytest
from repeat_class import repeat
@pytest.mark.parametrize("times, expected", [
(3, 3),
(1, 1),
])
def test_repeat_call_count(times: int,
expected: int) -> None:
calls: list[str] = []
@repeat(times=times)
def record() -> None:
calls.append("call")
record()
assert len(calls) == expected
@pytest.mark.parametrize("times", [0, -1])
def test_repeat_rejects_times_below_one(times: int) -> None:
with pytest.raises(ValueError):
repeat(times=times)The class form has one limitation. A method decorated this
way becomes an instance rather than a function, and the call
then fails. trace_class.py and count_calls.py decorate
bare functions on purpose:
# method_decoration.py
from collections.abc import Callable
from exceptions import expect
class logged:
def __init__(self, func: Callable) -> None:
self.func = func
def __call__(self, *args: object,
**kwargs: object) -> object:
return self.func(*args, **kwargs)
class Ex:
@logged
def method(self, x: int) -> int:
return x
ex = Ex()
expect(TypeError, ex.method, 5)
#: [TypeError] Ex.method() missing 1 required positional
#: argument: 'x'Ex.method is a logged instance,
stored as a class attribute. Python binds a class attribute to
an instance only when that attribute is a descriptor,
an object with a __get__() method, and every
ordinary function is one. A logged instance has no
__get__(), so ex.method hands back the
logged instance unbound, and
ex.method(5) really calls
logged.__call__(logged_instance, 5).
self.func runs with 5 as its only
argument, so 5 fills method’s own
self parameter and leaves nothing for
x. The Ex instance never arrives. The
TypeError blames a missing x, with no
hint that the real cause is a missing __get__(). Metaprogramming
shows the descriptor protocol, which a class-based decorator
must implement to work on methods.
Because a function is a descriptor, wrapper() in
the function form binds to an instance like any other
method:
# method_function_form.py
from tracer import trace
class Ex:
@trace
def method(self, x: int) -> int:
return x
def __repr__(self) -> str:
return "Ex()"
ex = Ex()
print(ex.method(5))
#: -> method(Ex(), 5)
#: <- method = 5
#: 5self arrives as the first traced argument, and
the __repr__() the class defines prints it as
Ex(), so the same decoration that failed as a class
works here with no descriptor of your own. For the same reason,
repeat_class.repeat escapes the limitation. Its
__call__() returns wrapper, an
ordinary function, so a method decorated with
@repeat(times=3) is still a function. With a fully
typed class-based decorator, like
trace_class.trace, the type checker reports a
missing argument and a type mismatch on a call like
ex.method(5), catching the same problem.
@trace with no arguments calls
trace(add). The function goes straight to the
constructor. @repeat(times=3) calls
repeat(times=3) first, producing an instance, then
applies that instance to greet. The arguments go to
the constructor, and the function arrives later, at
__call__(). Gaining arguments moves the function’s
arrival from __init__() to __call__().
The function form hides this shift inside an extra nested
def. The class form makes it visible.
Outside of methods, the form you choose is mostly a matter of
taste. Both forms preserve the wrapped function’s exact
signature for the type checker, using the same **P
and R type parameters, and both stack, the way stacking.py stacks two
function-form decorators. The function form is more compact. The
class form reads better when the decorator carries state or
grows complicated, because the phases are separate methods
instead of nested closures.
The class form with arguments scales up to small frameworks.
A build tool can offer a @rule(target, *deps)
decorator whose constructor records the target and its
dependencies, and whose __call__() registers the
decorated function in a class-level table with that metadata. A
driver walks the table later and runs the rules in order, so the
decorator becomes the registration mechanism for the whole
system.
The descriptor limitation is the one hard reason to choose
the function form. A decorator meant for methods either returns
a function, as the function form and
repeat_class.repeat both do, or implements
__get__().
A context manager can also decorate a function, bracketing every call with its setup and cleanup code.
Every example so far decorates a function. A
class statement takes a decorator the same way, and
the decorator receives the class object. Decorating a class
differs from Decorators as
Classes, where the class is the decorator. Here the
decorator is an ordinary function, and the class is the thing
decorated. The register() decorator records every
class it decorates in registry:
# register.py
registry: dict[str, type] = {}
def register[T](cls: type[T]) -> type[T]:
registry[cls.__name__] = cls
return cls
@register
class Espresso:
...
@register
class Latte:
...
if __name__ == "__main__":
print(sorted(registry))
#: ['Espresso', 'Latte']register() returns cls unchanged,
so this decoration adds no wrapper. register()
exists for the side effect of recording the class. The type
parameter T does for a class decorator what
**P and R do for a function decorator.
If register’s annotation is
(cls: type) -> type, register()
hands back a bare type, and ty and
Pyright see Espresso() as an Any. A
class decorator can also return a replacement class, just as a
function decorator returns a replacement function.
A registry filled this way is as complete as the imports that
ran. A class in a module nobody imported stays out of the
registry. Keying on cls.__name__ also means two
same-named classes from different modules overwrite each other.
Factory
returns to both.
Metaprogramming
shows __init_subclass__(), which builds a registry
like this without a decorator.
The tests confirm that register() hands back the
class it receives, and that registry maps each
class name to its class:
# test_register.py
from register import Espresso, Latte, register, registry
def test_register_returns_same_class() -> None:
assert register(Espresso) is Espresso
def test_registry_looks_up_by_name() -> None:
assert registry["Espresso"] is Espresso
assert registry["Latte"] is Latte@ Does Not
Require@ constrains the statement below it. A decorator
line must sit directly above a def or a
class. @decorator above a bare
assignment, or above a type alias, is a syntax
error rather than a decorator applied to something unusual. Past
that, @ places no requirement on the callable it
passes to the decorator. The callable you decorate can come from
somewhere other than a def:
# lambda_decoration.py
from collections.abc import Callable
def report(
func: Callable[[int], int]) -> Callable[[int], int]:
def wrapper(n: int) -> int:
print(f"Calling {func.__name__} with {n}") # type: ignore
return func(n)
return wrapper
double = report(lambda n: n * 2)
@report
def triple(n: int) -> int:
return n * 3
if __name__ == "__main__":
print(double(21))
print(triple(21))
#: Calling <lambda> with 21
#: 42
#: Calling triple with 21
#: 63report requires a callable. func
can come from anywhere. Calling report directly,
instead of through @, decorates the
lambda in place. @ is convenient sugar
for the common case of decorating a fresh def, not
a requirement. The same call decorates a
functools.partial, a bound method, or an instance
of a class with __call__(), since all a decorator
receives is a callable. Calling the result is another matter.
report’s wrapper reads func.__name__,
which a partial and a callable instance lack, so
those two raise an AttributeError at the call,
while the function and the bound method run.
The return side is equally unconstrained. A decorator “returns a result, which Python binds to the original name,” and that result need not be callable:
# run_once.py
from collections.abc import Callable
def run_once[T](func: Callable[[], T]) -> T:
return func()
@run_once
def greeting() -> str:
return "Hello, world"
if __name__ == "__main__":
print(greeting)
print(type(greeting).__name__)
#: Hello, world
#: strrun_once calls greeting
immediately, at decoration time, and hands back whatever
greeting() returned. After decoration the name
greeting refers to that str, so
greeting() raises a TypeError. A
str is not callable.
run_once pays off for a value that needs
one-time setup logic but stays constant afterward. For anything
simpler, a module-level constant computed the ordinary way reads
better.
Since Python 3.9 (PEP 614), the expression after
@ can be any expression, so you can write
run_once inline as @lambda:
# lambda_decorator.py
@lambda f: f()
def fib_table() -> list[int]:
table = [0, 1]
while len(table) < 10:
table.append(table[-1] + table[-2])
return table
print(fib_table)
#: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]The run-once idiom is the most common real use of
@lambda. The practical reason for it is scope. The
loop needs a working variable, table, and building
the list inside a function keeps table local. The
module gains one name, fib_table, holding the
finished list.
Treat @lambda as a curiosity and avoid it. A
reader who sees def fib_table() expects a function
and finds a list. The type checker loses track too. The lambda’s
parameter has no annotation, so ty reports
fib_table as Unknown, where the
generic run_once carries T through to
str. If you want the run-once idiom, use the named
decorator.
A decorator can replace a class the same way. Singleton
replaces a class with a callable object that stands in for it.
The first call constructs one instance, and every later call
returns that same instance. The name that follows
class then refers to that callable object, not to a
class.
The @ syntax decorates a function or class once,
at definition, so every call or every instance gets the
wrapping. Sometimes you want to choose later: add
responsibilities to individual objects at runtime, and let each
caller decide which responsibilities to add. That is the
object-oriented Decorator pattern.
Consider a pizza shop. A class for every pizza-and-topping combination explodes: Margherita, Margherita with olives, Margherita with olives and feta, and so on. Each new topping doubles the menu.
Instead, the toppings become decorators. A plain pizza knows its own cost and description. A topping wraps a pizza at runtime, adds to the cost, and adds to the description. Because a topping is a pizza, you can wrap a topping in another topping.
# pizza_decorator.py
from typing import ClassVar, Protocol
class Pizza(Protocol):
@property
def cost(self) -> float: ...
@property
def description(self) -> str: ...
class Margherita:
cost = 8.00
description = "Margherita"
class Hawaiian:
cost = 9.50
description = "Hawaiian"
class Topping:
add_cost: ClassVar[float] = 0.0
def __init__(self, pizza: Pizza) -> None:
self.pizza = pizza
self.name = type(self).__name__
@property
def cost(self) -> float:
return self.pizza.cost + self.add_cost
@property
def description(self) -> str:
return f"{self.pizza.description} + {self.name}"
class Garlic(Topping):
add_cost = 0.50
class Olives(Topping):
add_cost = 0.75
class Feta(Topping):
add_cost = 1.25
if __name__ == "__main__":
order = Feta(Olives(Margherita()))
print(f"{order.description}: ${order.cost:.2f}")
haw = Garlic(Feta(Hawaiian()))
print(f"{haw.description}: ${haw.cost:.2f}")
#: Margherita + Olives + Feta: $10.00
#: Hawaiian + Feta + Garlic: $11.25Feta(Olives(Margherita())) is the object version
of stacked @ decorators. Each topping wraps the
pizza inside it and forwards through the same two-property
interface, cost and description. The
Pizza Protocol describes that
interface. Both the plain pizzas and the toppings satisfy it
structurally, with no shared base class required. This is structural
typing.
A read-only @property in a Protocol
requires that reading the name produce the property’s return
type, and lets each class choose how. Margherita
supplies cost as a class attribute and
Topping computes it in a property. Both read as a
float, so both match.
Topping.__init__() sets
self.name = type(self).__name__, reading each
subclass’s own name at construction time instead of repeating it
as a string. Garlic, Olives, and
Feta do not mention their own names.
Adding a new topping means adding one class with one line,
add_cost. Changing the price of a topping means
changing one number, in one place. Compare that to a class per
combination, where a price change touches every class that
includes that topping.
A Pizza with a
toppings: list[Topping] field, summing each
topping’s add_cost and joining its name, solves the
same combinatorial problem, with no wrapping and no
Protocol. Here, where a topping contributes data (a
number and a name), that list is the simpler design. The
Decorator pattern earns its structure when a topping
needs behavior, not just data: a topping that changes how
cost rounds, adds a description only under some
condition, or must be handed elsewhere as a Pizza.
A list of toppings is data that a pizza holds. A decorator chain
is a pizza, each layer satisfying the same interface the plain
pizzas do.
Factory has
its own Pizza, a frozen data class that a
PizzaBuilder assembles, to illustrate the unrelated
Builder pattern. The two examples share a topic, not a
type.
A Decorator keeps the wrapped object’s interface and adds behavior. Proxy, Adapter, and Façade wrap the same way and differ in intent.
The tests check the cost and description of a stacked order,
Feta(Olives(Margherita())), and of a single
topping, Garlic(Margherita()):
# test_pizza_decorator.py
import pytest
from pizza_decorator import Feta, Garlic, Margherita, Olives
def test_stacked_toppings() -> None:
order = Feta(Olives(Margherita()))
assert order.cost == pytest.approx(10.00)
assert order.description == (
"Margherita + Olives + Feta")
def test_single_topping() -> None:
order = Garlic(Margherita())
assert order.cost == pytest.approx(8.50)
assert order.description == "Margherita + Garlic"Several decorators elsewhere in this book use this mechanism.
@property,
@cached_property, @staticmethod,
and @classmethod
each wrap a function the same way trace does, but
return a descriptor instead of a plain wrapper. That lets them
change how attribute access behaves, and makes them work on
methods, where a __call__-based class like
logged fails. @dataclass
is a class decorator like register, except it
mutates the class instead of leaving it unchanged, adding a
generated __init__(), __repr__(), and
__eq__() to the same object it received. @functools.cache
and @functools.lru_cache wrap a function the way
add_behavior does, in a wrapper that holds
func and calls it, storing results in a memo
dictionary instead of printing around the call. Understanding
any of these needs no new syntax.
The one piece of machinery left for later is the descriptor
protocol that @property,
@cached_property, @staticmethod, and
@classmethod implement. Metaprogramming
takes it up.
Every decorator costs two things wraps does not
remove, since wraps copies metadata and leaves the
extra call in place. A traceback through a decorated function
shows wrapper, one more frame than the caller and
the original body alone show. Each call also pays for an extra
Python-level function call, the wrapper’s own, before the real
body runs. Neither matters for a function called occasionally.
Both add up for one called in a tight loop, and stacking
decorators multiplies both by the number of layers.
This chapter’s solutions give a hint, usually the shape of the code, and a full answer for each exercise.
announce that prints
the name of each class it decorates and returns it unchanged,
then apply it to two small classes. Compare what it can do to
what register does.timing decorator that prints how long
the wrapped function took, using
time.perf_counter(). Apply it together with
@trace and predict the order of the output.trace as a class-based decorator that
also keeps a class-level counter shared across every decorated
function, and report the total number of traced calls in the
program. Note where the shared state lives compared to the
per-instance count in
count_calls.memo decorator that works both with and
without parentheses, so @memo and
@memo(maxsize=10) both decorate a function. Cache
each result in a dictionary keyed by the arguments, and drop the
oldest entry once the cache holds more than maxsize
of them. Distinguish the two forms by checking whether a first
argument arrived.retry(times) decorator in the function
form that calls the wrapped function again when it raises an
exception, up to times attempts, and re-raises the
last exception when they all fail. Check that
__name__ survives.repeat from repeat_class.py, and a
second method with logged from method_decoration.py. Call
the first through an instance, then print the type of the object
each method name refers to in the class. Explain why one
class-based decorator works on a method and the other does
not.