Lesson 5 · 5 of 20
Decorators · Python
Add logging and permission checks to Python functions without touching them: decorators, decorators with arguments and functools.wraps.

A decorator is a function that takes a function and returns another one that wraps it. log_calls(func) defines wrapper, which calls the original, prints its name and result, and returns it. It is a closure: wrapper remembers func.
Writing @log_calls above a definition is the same as refund = log_calls(refund): from then on, the name refund points to the wrapper. @functools.wraps(func) copies the original name and docstring onto the wrapper; without it, refund.__name__ would be "wrapper".
require_role("admin") is a decorator with arguments: a function that returns the real decorator. That is why there are three levels: require_role takes the role, decorator takes the function and wrapper takes the arguments of each call. The wrapper checks user["roles"] and, if the role is missing, never calls the function at all.
Decorators stack, and the bottom one is applied first. refund is guarded by require_role, and the whole thing is wrapped by log_calls, so the log also records denied calls, as you can see with Luis.
This separates what a function does (work out a refund) from what many functions repeat (logging, permission checks, timing, retries).
Code · main.py
import functoolsdef log_calls(func): @functools.wraps(func) # keep the original name and docstring def wrapper(*args): result = func(*args) print(f"log: {func.__name__} -> {result}") return result return wrapperdef require_role(role): # a decorator with arguments def decorator(func): @functools.wraps(func) def wrapper(user, *args): if role not in user["roles"]: return f"denied: {user['name']} is not {role}" return func(user, *args) return wrapper return decorator@log_calls # decorators stack: bottom one first@require_role("admin")def refund(user, amount): return f"{user['name']} refunded {amount:.2f} EUR"ana = {"name": "Ana", "roles": ["admin", "sales"]}luis = {"name": "Luis", "roles": ["sales"]}print(refund(ana, 30))print(refund(luis, 30))print("Name kept:", refund.__name__)Output · python3 main.py
log: refund -> Ana refunded 30.00 EUR Ana refunded 30.00 EUR log: refund -> denied: Luis is not admin denied: Luis is not admin Name kept: refund
Key points
@decoabovedef fmeansf = deco(f).- The wrapper usually takes
*argsand**kwargsand returns the result. - Always use
@functools.wraps(func)on the wrapper. - A decorator with arguments is a function that returns a decorator.
- Stacked decorators are applied from the bottom up.
Exercise
Write an @uppercase decorator that turns the string returned by the function into capitals, and put it on refund above the other two.