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 are applied to functions, so that is where we start.
To apply a decorator, put @ followed by the
decorator name on the line above the definition. For simplicity,
we use an untyped Callable here:
# 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 behaviorLater, Maintaining the Wrapped Interface shows decorators with types.
The @hijack above cheese()
means:
cheese = hijack(cheese)
hijack returns doesnt_matter, which
Python assigns to the name cheese, so
cheese now refers to doesnt_matter.
Calling cheese() runs doesnt_matter
which never calls func and prints its own message
instead. The original cheese() behavior never
happens.
Since decoration binds the local function to the name
cheese, the local name
(doesnt_matter()) can be anything. The common
convention is to name 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 workwrapper() is a closure. Defined inside
add_behavior(), 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
add_behavior() has already returned. That is what
lets cheese(), called long after decoration
finished, still reach the original cheese function
through func. Closures
covers the general mechanism.
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.
This decorator traces calls:
# 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 = 5functools.wraps copies the original function’s
metadata onto the wrapper: its name, docstring, and other
attributes. Without it, the decorated add would
report its name as wrapper and lose its docstring,
misleading debuggers, help(), and documentation
tools. wraps is optional but there is rarely a
reason to omit it.
wraps keeps the runtime interface. The type
parameters (introduced in Static
Typing) 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] promises 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. You may only use them
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
checker accepts add(2, 3) but rejects
add("x") or add(2, 3, 4), even though
the body of wrapper() forwards anything. Without
**P you would fall back to
*args: Any, **kwargs: Any, and the wrapper would
swallow any arguments, discarding the signature the decorator is
meant to preserve.
The # type: ignore comments mark where the
checker cannot follow: a bare Callable is not
guaranteed to have a __name__ attribute, though
every actual function does.
Testing verifies that 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) == 5To 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")
#: Hello, Bob
#: Hello, Bob
#: Hello, BobThe return type is worth unpacking:
Callable[[Callable[P, R]], Callable[P, R]].
Callable[[A, B], X] reads as “a callable that takes
A and B and returns X”
(see the summary in Static Typing). The
first, inner brackets hold the parameter types, even when there
is only one, 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].” That describes
decorate: it takes func and returns
wrapper, both typed
Callable[P, R].
@repeat(times=3) first evaluates
repeat(times=3), which returns
decorate, the real decorator, which then wraps
greet. That wrapping happens when Python calls
decorate(greet), and only then does
decorate’s own body run, including its
@wraps(func) line. @wraps(func) is the
same two-step pattern one level down: call
wraps(func) to get a decorator, then apply it to
wrapper. Completing the outer decoration is what
triggers the inner one, but nothing calls itself. The nesting
stops at two levels, matching the two nested defs
in the source.
Inside wrapper(), the first call to
func happens before the loop, so
result always holds a value of type R
to return; the loop adds the remaining times - 1
calls. That first call happens unconditionally, so
times=0 and a negative times still
call func once, not zero times.
test_repeat.py parametrizes over
times, checking the ordinary case and both edge
cases in one test:
# test_repeat.py
import pytest
from repeat import repeat
@pytest.mark.parametrize("times, expected", [
(3, 3),
(1, 1),
(0, 1),
(-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) == expectedA decorator must 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, at decoration, 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
class trace[**P, R]:
__name__: str # Set by update_wrapper(), not __init__
def __init__(self, func: Callable[P, R]) -> None:
self.func = func
update_wrapper(self, func) # Copy __name__, __doc__, etc
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() does for a class
instance what functools.wraps does for a function.
wraps is a thin convenience layer over
update_wrapper() that copies the wrapped function’s
metadata.
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.
# 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, state between calls is natural. Here is a class-based decorator that counts calls. It 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()
print(hello.count) # The state lives on the decorator instance
#: call 1 of hello
#: hello
#: call 2 of hello
#: hello
#: 2Each @count_calls creates its own instance, so
the count on one decorated function never leaks into
another:
# 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 class form provides a valuable benefit 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:
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, BobWith decorator arguments, the class form is typically easier to reason about than the function form.
This shares repeat.py’s edge case: the first
call happens before the loop, so times=0 or a
negative times still calls func once.
test_repeat_class.py checks the same cases:
# test_repeat_class.py
import pytest
from repeat_class import repeat
@pytest.mark.parametrize("times, expected", [
(3, 3),
(1, 1),
(0, 1),
(-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) == expectedCompare the two cases. @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__(). The function form hides this shift
inside an extra nested def. The class form makes it
visible. The function moves from __init__() to
__call__() when the decorator gains arguments.
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. 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. That argument-capturing class-based decorator
scales up to small frameworks. A build tool or task runner can
offer a @rule(target, *deps) decorator. Its
constructor records the target and dependencies. Its
__call__() registers the decorated function in a
class-level table with that metadata. A driver later walks the
table to run things in order. The decorator becomes the
registration mechanism for the whole system.
A context manager can also act as a decorator, bracketing
every call with its setup and cleanup code. Context
Managers shows contextlib.ContextDecorator.
The class form has one real limitation: it does not work on
methods. None of trace, count_calls,
or repeat_class above was ever applied to a method,
only to a bare function, and that was not an accident:
# method_decoration.py
from collections.abc import Callable
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 Example:
@Logged
def method(self, x: int) -> int:
return x
example = Example()
try:
example.method(5)
except TypeError as e:
print(e)
#: Example.method() missing 1 required positional argument: 'x'Example.method is a Logged
instance, stored as a class attribute. Accessing it through
example.method does not bind it to
example, because a plain object is not a
descriptor: Python performs that binding only for
things implementing __get__(), which every ordinary
function does automatically. So example.method(5)
really calls Logged.__call__(logged_instance, 5).
self.func runs with 5 as its only
argument, and the Example 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
would need to implement to work on methods.
A function needs none of this: it is already a descriptor, so
wrapper() in the function form binds to an instance
like any other method. A fully typed class decorator, like
trace_class.trace, even gets the checker involved:
ty reports a missing argument and a type mismatch
on a call like example.method(5), catching the same
problem before the program runs.
You can apply more than one decorator. They nest from the bottom up:
# stacking.py
from repeat_class import repeat
from trace_class 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'This is 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.
Each decorator wraps the result of the one below it. 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.
test_stacking.py confirms both claims: the name
survives two layers of wrapping, and the inner decorator’s
repeat still happens once per outer call:
# test_stacking.py
from repeat_class import repeat
from trace_class 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"]You can apply a decorator to a class instead of a function.
This one registers every class it decorates, in
registry:
# 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))
#: ['Espresso', 'Latte']register() returns cls unchanged,
so this decoration adds no wrapper; it exists only for the side
effect of recording the class. A class decorator can also return
a replacement class, just as a function decorator returns a
replacement function.
Metaprogramming
shows __init_subclass__(), which builds a registry
like this without a decorator.
# 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@decorator above a def is shorthand
for name = decorator(name), as the
@hijack example showed at the start of this
chapter. Only a def or a class can
follow @; @decorator above a bare
assignment, or above a type alias, is a syntax
error rather than a decorator applied to something unusual. But
the decorator is only a function, and nothing requires the
callable it wraps to come from 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 only asks for a callable and never asks
how func was created. Calling it directly, instead
of through @, decorates the lambda on
the spot. @ 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 a decorator only cares that its argument is callable.
A second surprise sits on the return side. This chapter opened by saying a decorator “returns a result, which Python binds to the original name.” That result does not have to 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. greeting does not
survive decoration as a function: the name now refers to the
str that came out of it. Calling
greeting() again would fail, since a
str is not callable. This idiom pays off for a
value that needs one-time setup logic but stays constant
afterward. A module-level constant computed without a decorator
is usually clearer for anything simpler.
The same collapse happens to classes. Singleton
decorates a class with a callable that replaces it with one
cached instance, so the name that followed class
ends up bound to an object, not a type.
The @ syntax decorates a function or class once,
at definition, so every call or every instance gets the
wrapping. Sometimes you want the choice made 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, model the toppings as decorators. A plain pizza knows its own cost and description. A topping dynamically wraps a pizza, 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 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 = 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.
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 never mention their own names; the class name
already is the topping name.
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.
Factory has its own
Pizza, a frozen data class assembled by a
PizzaBuilder, to illustrate the unrelated Builder
pattern. The two examples share a topic, not a type.
# test_pizza_decorator.py
from pizza_decorator import Feta, Garlic, Hawaiian, Margherita, Olives
def test_plain_pizza() -> None:
pizza = Hawaiian()
assert pizza.cost == 9.50
assert pizza.description == "Hawaiian"
def test_stacked_toppings() -> None:
order = Feta(Olives(Margherita()))
assert order.cost == 10.00
assert order.description == (
"Margherita + Olives + Feta")
def test_single_topping() -> None:
order = Garlic(Margherita())
assert order.cost == 8.50
assert order.description == "Margherita + Garlic"Several decorators from earlier chapters use this mechanism.
@property, cached_property,
@staticmethod, and @classmethod (see
Classes, Classes)
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 it also makes
them work correctly on methods, where a
__call__-based class, like Logged
above, does not. @dataclass (see Data Classes
as Types) is a class decorator like register,
except it returns a modified class instead of the same one
unchanged, adding a generated __init__(),
__repr__(), and __eq__().
functools.cache and
functools.lru_cache (see Performance) wrap a
function in the same closure-plus-func shape as
add_behavior, storing results in a memo dictionary
instead of printing around the call. None of these needed new
syntax to understand. They are ordinary decorators, built from
the closures and callables this chapter covered.
Mushroom topping (cost 0.60) and use it
to build a Hawaiian with mushroom and feta.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.trace_class.trace a __get__()
method so it works correctly when applied to an instance method.
Apply it to a method on a small class and confirm
self arrives correctly.