Decorators — Wrapping Functions with Functions
Advanced@decorator replaces a function with a wrapped version — the mechanism behind logging, timing, caching, auth checks, and every @app.get you'll write in FastAPI.
Overview
A decorator is a function that takes a function and returns a replacement — @deco above def f is exactly f = deco(f). Combined with closures (the wrapper remembers the original) and *args/**kwargs (the wrapper forwards anything), decorators add behaviour around functions without touching their code. functools.wraps preserves the original's name/docstring — skip it and debugging tools lie to you. Parameterized decorators (@retry(times=3)) add one more nesting level: a factory that returns a decorator.
The Pattern, Step by Step
wrapper(*args, **kwargs) accepts anything, adds behaviour, and forwards the call. @wraps(func) copies metadata. This template is 90% of every decorator ever written.
import time
from functools import wraps
def timed(func):
@wraps(func) # keep func's name/docs
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs) # call the original
ms = (time.perf_counter() - start) * 1000
print(f"{func.__name__} took {ms:.1f} ms")
return result
return wrapper
@timed # slow_sum = timed(slow_sum)
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000) # slow_sum took ~250 ms
print(slow_sum.__name__) # 'slow_sum' — thanks to @wraps
# without it: 'wrapper' (breaks debugging)Decorators with Arguments & Real Uses
@retry(times=3) calls retry(3) FIRST, which returns the actual decorator — three levels: factory → decorator → wrapper. This is the shape used by @app.get("/path") in FastAPI.
from functools import wraps
import time
def retry(times=3, delay=0.1): # 1) factory takes config
def decorator(func): # 2) decorator takes func
@wraps(func)
def wrapper(*args, **kwargs): # 3) wrapper runs it
last = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last = e
print(f"attempt {attempt} failed: {e}")
time.sleep(delay)
raise last
return wrapper
return decorator
@retry(times=3)
def flaky_api():
import random
if random.random() < 0.7:
raise ConnectionError("timeout")
return "data"
# Stacking — applied bottom-up: timed(retry(...)(f))
# @timed
# @retry(times=2)
# def fetch(): ...
# You already use decorators daily:
# @property, @staticmethod, @dataclass, @lru_cache, @app.get("/users")Key Points to Remember
- 1@deco is exactly f = deco(f) — a function replaced by its wrapped version
- 2Always @wraps(func) the wrapper — preserves __name__/__doc__ for debugging
- 3Parameterized decorators are factories: retry(3) returns the decorator
- 4Stacked decorators apply bottom-up
Interview Questions
Sign in to ask AriaWrite a decorator that logs arguments and return value of any function.
What breaks if you forget functools.wraps?
How does @retry(times=3) work — walk through all three nesting levels.
Ask Aria about Decorators — Wrapping Functions with Functions
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.