Decorators
Wrap a function to add behaviour — logging, timing, caching — without touching it.
- Explain that functions are values that can be passed and returned
- Read and write a simple decorator
- Apply built-in decorators with @
A decorator wraps a function to add behaviour — logging, timing, access checks, caching — without modifying the function's own code. It's the abstraction principle applied to functions: keep the core job pure, add the cross-cutting concern around it.
Functions are values
The idea rests on one fact: in Python, functions are ordinary values. You can pass them around and return them from other functions:
def shout(text):
return text.upper()
f = shout # f now refers to the function
f("hi") # "HI"A decorator wraps a function
A decorator is a function that takes a function and returns a new one that adds something around it:
def logged(func):
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
result = func(*args, **kwargs)
print("done")
return result
return wrapper
@logged
def add(a, b):
return a + bThe @logged line is sugar for add = logged(add). Now calling add(2, 3) runs
the wrapper — which logs, calls the original, logs again, and returns the result.
The *args, **kwargs let the wrapper accept whatever the wrapped function does.
Built-in decorators
You'll use decorators long before you write them. Common ones:
@staticmethod/@classmethodon classes.@propertyto expose a method like an attribute.@functools.cacheto memoise an expensive pure function automatically.
When writing your own, decorate the wrapper with @functools.wraps(func) so the
wrapped function keeps its name and docstring — otherwise tools and help() see
the wrapper instead.
Where to go next
Next: classes and dataclasses — bundling data and the behaviour that goes with it.