Python builds decoration into the language. You write
@something above a function or a class, and Python
wraps it. 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.
This chapter starts with the language feature, because that is where you meet decoration first. Then it shows the object-level pattern, for when you need to decorate individual objects at runtime.
@ SyntaxA decorator is a callable that takes a function and returns a function. The returned function usually does some work, calls the original, and does some more work. Here is a decorator that traces calls:
# trace.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:
print(f"-> {func.__name__}{args}") # 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, 3)The output is:
-> add(2, 3)
<- add = 5
The @trace above add means:
add = trace(add)
trace returns wrapper, so the name
add now refers to wrapper. Calling
add(2, 3) runs the wrapper, which prints, calls the
real add, prints again, and returns the result.
functools.wraps copies the original function’s name
and docstring onto the wrapper, so the wrapped function still looks
like itself when you inspect it.
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]]:
def decorate(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(times - 1):
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")@repeat(times=3) calls repeat(3), which
returns the real decorator, which then wraps greet. The
greeting prints three times.
A decorator only has to be a callable that takes a function and
returns a callable. 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, when the function is decorated, and __call__ runs
on every call to the decorated function. Here is the
trace decorator written as a class:
# trace_class.py
from collections.abc import Callable
from functools import update_wrapper
from typing import Any
class trace:
def __init__(self, func: Callable[..., Any]) -> None:
self.func = func
update_wrapper(self, func) # copy __name__, __doc__, etc.
def __call__(self, *args: Any, **kwargs: Any) -> Any:
print(f"-> {self.func.__name__}{args}") # 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, 3)@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 does for a class instance what
functools.wraps does for a function: it copies the
wrapped function’s name and docstring across.
Because the instance can hold attributes, state between calls is natural. A class decorator that counts calls keeps the count on the instance, with no closure trick:
# count_calls.py
from collections.abc import Callable
from functools import update_wrapper
from typing import Any
class count_calls:
def __init__(self, func: Callable[..., Any]) -> None:
self.func = func
self.count = 0
update_wrapper(self, func)
def __call__(self, *args: Any, **kwargs: Any) -> Any:
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()
print(hello.count) # 2: the state lives on the decorator instanceThe class form shifts in an important way when the decorator
itself 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
from typing import Any
class repeat:
def __init__(self, times: int) -> None:
self.times = times # the decoration arguments
def __call__(
self, func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
result = None
for _ in range(self.times):
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")Compare the two cases. @trace with no arguments
calls trace(add): the function goes straight to the
constructor. @repeat(times=3) calls
repeat(3) first, producing an instance, then applies
that instance to greet: the arguments go to the
constructor, and the function arrives later, at
__call__. The function form hides this shift inside an
extra nested def. The class form makes it visible: the
function moves from __init__ to __call__
the moment the decorator gains arguments.
Which form to use is mostly taste. The function form is more
compact, and with ParamSpec it preserves the wrapped
function’s exact signature for the type checker, where the class
form erases it to Callable[..., Any]. The class form
reads better when the decorator carries state or grows complicated,
because the phases are separate methods instead of nested closures.
That argument-capturing class decorator scales up to small
frameworks: a build tool or task runner can offer a
@rule(target, *deps) decorator whose constructor
records the target and dependencies, whose __call__
registers the decorated function in a class-level table with that
metadata, and whose driver later walks the table to run things in
order. The decorator becomes the registration mechanism for the
whole system.
You can apply more than one decorator. They nest from the bottom up:
@trace
@repeat(times=2)
def f() -> None: ...
This is f = trace(repeat(2)(f)). Each decorator
wraps the result of the one below it. That nesting is the
transparency the pattern depends on. Every layer presents the same
interface, so the layers compose.
A decorator can take a class instead of a function. This one records every class it is applied to:
# register.py
registry: dict[str, type] = {}
def register(cls: type) -> type:
registry[cls.__name__] = cls
return cls
@register
class Espresso:
...
@register
class Latte:
...
if __name__ == "__main__":
print(sorted(registry))The output is ['Espresso', 'Latte']. The Metaprogramming chapter shows
__init_subclass__, which builds a registry like this
without a decorator.
The @ syntax decorates a function or class once, at
definition. Every call or every instance gets the wrapping.
Sometimes you want to add responsibilities to one object at runtime,
and let the caller choose which responsibilities to add. That is the
object Decorator pattern.
Consider a coffee shop. A class for every drink-and-extra combination explodes: espresso, espresso with whipped cream, decaf espresso with whipped cream, and so on. Each new extra doubles the menu.
Instead, model the extras as decorators. A plain drink knows its own cost and description. An extra wraps a drink, adds to the cost, and adds to the description. Because an extra is itself a drink, you can wrap an extra in another extra.
# coffee.py
from typing import Protocol
class Drink(Protocol):
@property
def cost(self) -> float: ...
@property
def description(self) -> str: ...
class Espresso:
cost = 1.50
description = "espresso"
class Cappuccino:
cost = 1.75
description = "cappuccino"
class Extra:
"Base object decorator: wraps a Drink and adds to it."
add_cost = 0.0
name = ""
def __init__(self, drink: Drink) -> None:
self.drink = drink
@property
def cost(self) -> float:
return self.drink.cost + self.add_cost
@property
def description(self) -> str:
return f"{self.drink.description} + {self.name}"
class Whipped(Extra):
add_cost = 0.50
name = "whipped cream"
class Decaf(Extra):
add_cost = 0.0
name = "decaf"
class ExtraShot(Extra):
add_cost = 0.75
name = "extra shot"
if __name__ == "__main__":
order = Whipped(ExtraShot(Espresso()))
print(f"{order.description}: ${order.cost:.2f}")
plain = Cappuccino()
print(f"{plain.description}: ${plain.cost:.2f}")The output is:
espresso + extra shot + whipped cream: $2.75
cappuccino: $1.75
Whipped(ExtraShot(Espresso())) is the object version
of stacked @ decorators. Each extra wraps the drink
inside it and forwards through the same two-property interface,
cost and description. The
Drink Protocol describes that interface.
Both the plain drinks and the extras satisfy it structurally, with
no shared base class required. This is the structural typing from
the Static Type Checking
chapter.
Adding a new extra means adding one class. Changing the price of an extra means changing one number, in one place. Compare that to a class per combination, where a price change touches every class that includes that extra.
# test_coffee.py
from coffee import Cappuccino, Decaf, Espresso, ExtraShot, Whipped
def test_plain_drink() -> None:
cap = Cappuccino()
assert cap.cost == 1.75
assert cap.description == "cappuccino"
def test_stacked_extras() -> None:
order = Whipped(ExtraShot(Espresso()))
assert order.cost == 2.75
assert order.description == (
"espresso + extra shot + whipped cream")
def test_decaf_adds_no_cost() -> None:
order = Decaf(Espresso())
assert order.cost == 1.50
assert order.description == "espresso + decaf"Syrup extra (cost 0.30) and use it to build a
decaf latte with syrup.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 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.