Decorators
Advanced · 20 min read · ▶ live playground · ✦ checkpoint
A single line above a function can add logging, access checks, caching, or timing to every call. A decorator is a callable - a value you can call with parentheses - that takes a function or class and returns a replacement, usually a wrapped version of the original.
Start with closures
A closure is a function that remembers names from the scope where it was created, even after that outer function has finished. You already know scopes as rooms with name tags on the walls. A closure is a recipe card that leaves the room carrying the name tags it still needs.
def make_bonus_adder(bonus):
def add_bonus(score):
return score + bonus
return add_bonus
add_five = make_bonus_adder(5)
add_ten = make_bonus_adder(10)
print(add_five(80))
print(add_ten(80))
print(add_five.__name__)8590add_bonus
make_bonus_adder returns the inner function. That inner function still remembers the bonus value from the call that created it. add_five carries 5; add_ten carries 10.
This is the foundation decorators stand on: one function can build and return another function with remembered settings.
Write your first decorator
A wrapper function is an inner function that calls another function while adding behavior before, after, or around that call. A decorator is like a reusable wrapper around a recipe card: the original recipe still cooks the meal, but the wrapper adds steps such as "announce start" and "announce finish."
from functools import wraps
def announce(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"starting {function.__name__}")
result = function(*args, **kwargs)
print(f"finished {function.__name__}")
return result
return wrapper
@announce
def award_points(player, points):
"""Build one score line."""
return f"{player} earned {points} points"
print(award_points("Mina", 30))
print(award_points.__name__)
print(award_points.__doc__)starting award_pointsfinished award_pointsMina earned 30 pointsaward_pointsBuild one score line.
The @announce line is decorator syntax. It means: define award_points, then rebind the name to announce(award_points). Rebinding uses the same name-tag model from variables: the name now points at the wrapped function.
*args and **kwargs let the wrapper accept the same positional and keyword arguments as the original function. The wrapper passes them through, stores the return value, and returns it.
Metadata is data about a function, such as its __name__ and docstring. functools.wraps is a helper that copies the original function's metadata onto the wrapper, so tools and readers still see award_points, not wrapper.
Use decorators with arguments
A decorator factory is a function that receives settings and returns a decorator. Think of it as a wrapper station with a settings dial: choose "coach" first, then the station builds the exact wrapper you need.
This pattern powers access control, which means rules that decide who is allowed to run an action. It also pairs naturally with logging, which means recording what code did so you can understand a run later.
from functools import wraps
current_user = {"name": "Ada", "role": "member"}
def log_call(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"calling {function.__name__}")
return function(*args, **kwargs)
return wrapper
def require_role(role):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
if current_user["role"] != role:
return f"Access denied for {current_user['name']}"
return function(*args, **kwargs)
return wrapper
return decorator
@log_call
@require_role("coach")
def reset_score(player):
return f"{player}'s score was reset"
print(reset_score("Ravi"))
current_user["role"] = "coach"
print(reset_score("Ravi"))calling reset_scoreAccess denied for Adacalling reset_scoreRavi's score was reset
@require_role("coach") calls require_role first, and that returns the real decorator. Stacking decorators means putting more than one decorator on the same function. Python applies the lower decorator first, then the one above it. At call time, the top wrapper is the first one you enter.
Try it - cache one report
A cache is stored data you reuse later instead of recomputing the same result. Run this, then add one more repeated call for "Mina" and watch the cache branch run.
The cache dictionary lives in the closure. Call one builds and stores the report. Call two uses the remembered result. A fuller standard-library caching tool, functools.lru_cache, comes in lesson 12.4.
Time work without trusting the clock
A timing decorator measures how long a function call takes. Real timing uses a changing clock, so this example uses a fake clock to keep the output exact while showing the pattern.
from functools import wraps
fake_times = [100.0, 100.4]
def fake_clock():
return fake_times.pop(0)
def timed(clock):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
start = clock()
result = function(*args, **kwargs)
end = clock()
print(f"{function.__name__} took {end - start:.1f} seconds")
return result
return wrapper
return decorator
@timed(fake_clock)
def total_apples(baskets):
total = 0
for apples in baskets:
total = total + apples
return total
print(total_apples([3, 5, 2]))total_apples took 0.4 seconds10
The decorator does not care what total_apples calculates. It cares about the shape: call a function, measure before and after, return the original result.
Decorate classes too
A class decorator is a decorator that receives a class and returns the replacement bound to that class name, usually the same class or a modified class. Reuse the class-as-blueprint model: a class decorator is a stamp on the blueprint before anyone builds an instance from it.
def add_badge(cls):
cls.badge = "advanced"
return cls
@add_badge
class PlayerCard:
def __init__(self, player, points):
self.player = player
self.points = points
def label(self):
return f"{self.player} ({self.badge}): {self.points} points"
print(PlayerCard.badge)
card = PlayerCard("Mina", 120)
print(card.label())advancedMina (advanced): 120 points
You have already used class decorators: @dataclass in lesson 11.5 takes a class, adds useful methods, and returns the improved class. The syntax is the same idea whether the target is a function or a class.
Checkpoint
Answer all three to mark this lesson complete
Decorators let you attach behavior at the edge of a function or class while keeping the core action readable. Next, context managers give the same kind of clean syntax to setup and cleanup, especially around resources that must be closed.