Decorators
Wrapping functions to add behaviour.
5 minute read
Big idea
A decorator wraps a function in another function to add behaviour — logging, timing, checking — without changing the original's code. @name above a def applies it.
Functions are values
Two facts make decorators possible. A function can be passed around like any value, and a function can be defined inside another and returned:
def make_multiplier(k):
def multiply(x):
return x * k
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5))output
10 15Run and edit it in the app.
multiply remembers the k it was made with, even after make_multiplier has returned. That's called a closure.
A decorator by hand
A decorator is a function that takes a function and returns a new, wrapped one:
def shout(func):
def wrapper(name):
result = func(name)
return result.upper() + "!"
return wrapper
def greet(name):
return f"hello, {name}"
greet = shout(greet)
print(greet("ada"))output
HELLO, ADA!Run and edit it in the app.
greet = shout(greet) replaces greet with the wrapper, which calls the original inside it.
The @ syntax
@shout above a def is exactly the same as writing greet = shout(greet) after it:
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}{args}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
total = add(2, 3)
print(total * 10)What does it print? Show the answer
calling add(2, 3)
add returned 5
50Run and edit it in the app.
*args, **kwargs let the wrapper accept whatever arguments the original takes and pass them straight through — so one decorator works on any function.
Remember to return
def announce(func):
def wrapper(*args):
print("Running", func.__name__)
func(*args)
return wrapper
@announce
def square(n):
return n * n
print(square(6))output
Running square
36Run and edit it in the app.
The wrapper calls func but throws away its result, so square(6) returns None. Write return func(*args).
Decorators you'll see
You'll use more decorators than you write:
@dataclass— from the OOP module.@functools.cache— remembers results so repeated calls are instant.@property,@staticmethod,@classmethod— inside classes.- Web frameworks use them for routes:
@app.get("/").
When you write your own, add @functools.wraps(func) above def wrapper so the wrapped function keeps its name and docstring.
@timer above def run(): ... is the same as…
Watch it
Think of it like
Gift wrapping. The present inside is unchanged, but the wrapping adds something on the outside — a bow, a tag. And you can wrap any present the same way.
Key terms
- Decorator — A function that takes a function and returns a wrapped version.
- Closure — An inner function that remembers variables from where it was made.
@syntax — Applies a decorator to the function defined below it.
Check yourself
What does a decorator receive?
Why does a wrapper usually take *args, **kwargs?
A wrapper calls func(x) but doesn't return it. What does the decorated function return?