Decorators
shout takes a function and returns a new function (wrapper) that calls the original and modifies its result. This pattern — wrap, call the original, do something before/after/with the result — is the entire idea behind decorators. The @ syntax is sugar for exactly the reassignment shown above.
🎯 What you'll learn
- A Decorator Is Just a Function That Wraps Another Function
- The
@Syntax - Preserving Metadata with
functools.wraps - Multiple Decorators Stack Bottom-Up
- Common Uses
1A Decorator Is Just a Function That Wraps Another Function
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def greet(name):
return f"hello, {name}"
greet = shout(greet) # manually "decorating" — reassign the name
print(greet("ana")) # "HELLO, ANA"shout takes a function and returns a new function (wrapper) that calls the original and modifies its result. This pattern — wrap, call the original, do something before/after/with the result — is the entire idea behind decorators. The @ syntax is sugar for exactly the reassignment shown above.
2The @ Syntax
def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
return f"hello, {name}"
print(greet("ana")) # "HELLO, ANA"@shout
def greet(name):
return f"hello, {name}"
# is EXACTLY equivalent to:
def greet(name):
return f"hello, {name}"
greet = shout(greet)@decorator_name above a def runs decorator_name(the_function_just_defined) immediately after the function is compiled, and rebinds the name to whatever that call returns. This happens once, at definition time — not on every call to greet.
3Preserving Metadata with functools.wraps
import functools
def shout(func):
@functools.wraps(func) # copies __name__, __doc__, etc. from func onto wrapper
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
"""Return a greeting."""
return f"hello, {name}"
print(greet.__name__) # "greet" — WITHOUT @wraps this would be "wrapper"
print(greet.__doc__) # "Return a greeting."Without functools.wraps, the decorated function's __name__, __doc__, and other introspection metadata get silently replaced by wrapper's — breaking debuggers, help(), and any code that inspects the function. Always use @functools.wraps(func) inside a decorator's inner function.
4Multiple Decorators Stack Bottom-Up
def bold(func):
def wrapper(*a, **kw):
return f"<b>{func(*a, **kw)}</b>"
return wrapper
def italic(func):
def wrapper(*a, **kw):
return f"<i>{func(*a, **kw)}</i>"
return wrapper
@bold
@italic
def text():
return "hi"
print(text()) # "<b><i>hi</i></b>"Stacked decorators apply bottom-to-top at definition time (the one closest to the function runs first, wrapping it; the next one wraps that result), but that means the resulting call order is outside-in: bold's wrapper runs first when you call text(), and it calls into italic's wrapper, which calls the real text.
5Common Uses
import time, functools
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timed
def slow_add(a, b):
time.sleep(0.1)
return a + bCommon real-world decorators: logging/timing (above), caching (functools.lru_cache), access control/authentication checks, retry logic, input validation, and registering a function into some registry (e.g. route decorators in web frameworks like Flask/FastAPI).
⚠️ Common Pitfalls
- Forgetting
functools.wraps— breaks introspection,help(), and anything relying on__name__/__doc__; always include it. - Forgetting
*args, **kwargsin the wrapper — a wrapper defined asdef wrapper(): ...can only decorate zero-argument functions; almost always writedef wrapper(*args, **kwargs)unless you specifically need a fixed signature. - Assuming decorators run on every call — the decorator function itself runs exactly once, at definition time; only the returned
wrapperruns on every call. - Misreading stacking order —
@boldthen@italicabove a function appliesitalicfirst (closest to the def), butbold's wrapper executes first at call time (outermost).
1Decorators That Take Arguments — Three Layers of Functions
import functools
def repeat(times): # layer 1: takes the decorator's OWN arguments
def decorator(func): # layer 2: takes the function being decorated
@functools.wraps(func)
def wrapper(*args, **kwargs): # layer 3: the actual call-time wrapper
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(times=3)
def roll():
import random
return random.randint(1, 6)
print(roll()) # e.g. [4, 1, 6]@repeat(times=3) first calls repeat(times=3), which returns decorator — that is what actually gets applied to roll (decorator(roll)), exactly like an argument-less decorator from Part 1. A parameterized decorator is a function that returns a decorator, which is why it needs one extra layer of nesting compared to a plain decorator.
2Class-Based Decorators
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func) # class equivalent of @wraps
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@CountCalls
def greet():
return "hi"
greet(); greet()
print(greet.count) # 2Any object implementing __call__ can decorate a function — @CountCalls here means greet = CountCalls(greet), producing an instance of CountCalls that behaves like a callable, with the added benefit of easily holding extra state (self.count) that a closure-based decorator would need nonlocal for.
3Decorating Classes Themselves
def add_repr(cls):
def __repr__(self):
fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
return f"{cls.__name__}({fields})"
cls.__repr__ = __repr__
return cls
@add_repr
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
print(Point(1, 2)) # Point(x=1, y=2)A decorator can be applied to a class statement exactly like a function — it receives the class object itself and can return it modified, or return an entirely different object. dataclasses.dataclass is the most common real-world class decorator in the standard library, generating __init__/__repr__/__eq__ automatically.
4functools.lru_cache — A Decorator Built on These Ideas
import functools
@functools.lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
fib(30) # fast — memoized, exponential recursion collapses to linear work
print(fib.cache_info()) # CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)lru_cache is itself a parameterized decorator (section 1's pattern) implemented in C for speed in CPython, storing a dict keyed by the call's arguments (which must therefore be hashable). It demonstrates why the mutable-default-argument-style closures matter: the cache dict is created once, when lru_cache(maxsize=128) produces the actual decorator, and persists across every call to fib for the lifetime of that function object.
Under the HoodWhat @ Actually Compiles To▾
The @ syntax is evaluated top-to-bottom, then applied bottom-to-top, at def/class compile time
@bold
@italic
def text():
return "hi"compiles, roughly, to bytecode equivalent to:
def text():
return "hi"
text = italic(text) # closest decorator to the def applies FIRST
text = bold(text) # then the next one up wraps that resultBoth decorator expressions (bold, italic) are evaluated (looked up as names/attributes/calls) before either is applied, in top-to-bottom order — but the actual application (func = decorator(func)) happens bottom-to-top, working outward from the function definition. This is exactly why call order (section 4 of Part 1) ends up outside-in: the outermost decorator (bold) wraps a text that is already fully wrapped by italic.
There is no separate "decorator" concept at the bytecode/object level
Mechanically, a decorated function is completely ordinary once decoration finishes — there is no runtime flag, marker, or special object type indicating "this function was decorated." greet after @shout is just whatever shout(greet) returned; if that's a closure named wrapper, then greet is wrapper from that point on, full stop. Anything inspecting greet.__code__ after decoration sees wrapper's code object, not the original function's — which is precisely the introspection problem functools.wraps patches over (it copies __name__/__doc__/__dict__/__wrapped__ from the original onto the wrapper, and sets __wrapped__ so tools can still find the original if they know to look for it).
import functools
def shout(func):
@functools.wraps(func)
def wrapper(*a, **kw):
return func(*a, **kw).upper()
return wrapper
@shout
def greet(name):
return f"hi {name}"
print(greet.__wrapped__) # the ORIGINAL undecorated greet function object
print(greet.__wrapped__("ana")) # "hi ana" — bypasses the decorator entirelyParameterized decorators cost one extra closure layer, every time the module loads
@repeat(times=3) means repeat(times=3) actually executes at module-import time (or whenever the enclosing def/class is defined), producing a fresh decorator closure over times=3 — this closure allocation happens once per decorated definition, not once per call, using the same cell-object mechanism as any other closure (see the Variables and Scope topic's "Under the Hood" section).
⚠️ Common Pitfalls
- Forgetting the third layer for parameterized decorators —
@repeat(times=3)requiresrepeatto return a decorator, which itself returns a wrapper; skipping a layer produces aTypeErrorabout calling a function with the wrong number of arguments. - Applying
@functools.lru_cacheto a method with mutable/unhashable arguments — the cache key is built from the arguments; passing alistordictargument raisesTypeError: unhashable type. - Decorating instance methods without accounting for
self— a decorator's wrapper needs to passselfthrough via*args, **kwargslike any other positional argument; forgetting this breaks methods specifically (functions work fine, methods don't automatically special-case anything). - Assuming
@classmethod/@staticmethodcompose freely with custom decorators in any order — ordering matters; a custom decorator generally needs to be closer to the function (applied first / listed last, right abovedef) than@classmethod/@staticmethod, or it will receive the descriptor object instead of a plain callable.
📋 Quick Reference
def dec(func):
@functools.wraps(func)
def wrapper(*a, **kw): return func(*a, **kw)
return wrapper # plain decorator
def param_dec(opt):
def dec(func):
@functools.wraps(func)
def wrapper(*a, **kw): return func(*a, **kw)
return wrapper
return dec # parameterized decorator: 3 layers
@bold
@italic
def f(): ... # applied bottom-up: bold(italic(f))
f.__wrapped__ # original function, if @wraps was used
functools.lru_cache(maxsize=None) # built-in memoization decorator🎤 Interview Q&A
Q1What is a decorator?core▾
A callable that takes a function (or class) and returns a replacement, applied with @decorator. It adds behaviour like logging, caching, timing, retries or access checks without changing the function body.
- Equivalent without @?
f = decorator(f).
Q2Why use functools.wraps?core▾
It copies the wrapped function's __name__, __doc__, signature metadata and __wrapped__ to the wrapper, keeping introspection, debugging and docs correct.
- What breaks without it? Frameworks relying on names, docs and signatures.
Q3How do you write a decorator with arguments?core▾
Add another level: def retry(times): def deco(fn): @wraps(fn) def wrapper(*a, **k): … return wrapper; return deco, used as @retry(times=3).
- Class-based decorators? Classes implementing
__call__.
Q4Which built-in decorators do you use?senior▾
@property, @classmethod, @staticmethod, @functools.cache/lru_cache, @dataclass, @contextmanager, @functools.singledispatch, @abstractmethod.
- lru_cache caveat? Arguments must be hashable; cache lives for the process.
Q5How do decorators work with async functions?core▾
The wrapper must be async def and await the wrapped coroutine; check with inspect.iscoroutinefunction to support both sync and async.
- Common bug? Sync wrapper returning an un-awaited coroutine.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.