A decorator is a callable that takes a function and returns a (usually
wrapped) function, letting you add behaviour without modifying the original.
The @decorator syntax above a def is just sugar for reassigning the name to
the decorator's result: func = decorator(func).
def log_calls(func):
def wrapper(*args, **kwargs): # accept any signature
print(f"calling {func.__name__}")
return func(*args, **kwargs) # delegate to the original
return wrapper
@log_calls
def add(a, b):
return a + b
# equivalent to: add = log_calls(add)
add(2, 3) # prints "calling add", returns 5
This works because functions are first-class objects — they can be passed around and returned. Decorators are the idiomatic way to factor out cross-cutting concerns (logging, timing, caching, access control).
Without it, the wrapper replaces the original function's identity: the
decorated object reports the wrapper's __name__, __doc__, signature, and
__module__, which breaks introspection, debugging, and tools that rely on
metadata. functools.wraps copies that metadata from the original onto the
wrapper.
import functools
def log_calls(func):
@functools.wraps(func) # copy name, docstring, __wrapped__, etc.
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_calls
def greet():
"say hello"
...
greet.__name__ # "greet" (without wraps -> "wrapper")
greet.__doc__ # "say hello"
It also sets __wrapped__, so inspect.signature and unwrapping still work.
Rule of thumb: always apply @functools.wraps(func) to your wrapper — it's
effectively free and prevents subtle bugs.
You add another layer of nesting: an outer function takes the decorator's
arguments and returns the actual decorator, which takes the function and returns
the wrapper. So @repeat(3) first calls repeat(3) to get a decorator, which
is then applied to the function.
import functools
def repeat(n): # takes the decorator argument
def decorator(func): # takes the function
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3) # repeat(3) returns 'decorator'
def ping():
print("pong")
The mental model: @repeat(3) is ping = repeat(3)(ping) — three calls deep.
Remember the parentheses: @repeat(3) (with args) differs from @repeat (passing
the function directly), and forgetting them is a common bug.
A class becomes a decorator by being callable — define __call__. The
__init__ receives the decorated function; __call__ runs the wrapping logic on
each invocation. This is handy when the decorator needs to hold state (like a
call count) in a clean, attribute-based way.
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func) # the class-based wraps
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"call #{self.count}")
return self.func(*args, **kwargs)
@CountCalls
def hello():
print("hi")
hello(); hello() # "call #1" then "call #2"
hello.count # 2 — state lives on the instance
Use functools.update_wrapper (the function-form of wraps) to preserve
metadata. Class decorators shine for stateful decorators; for simple stateless
ones, a nested function with a nonlocal closure is usually lighter.
Decorators apply bottom-up (nearest the function first) at definition time, but the resulting wrappers execute top-down at call time. Stacking is just nested application: the top decorator wraps the result of the ones below it.
@a
@b
def f(): ...
# equivalent to: f = a(b(f)) — b wraps first, a wraps outermost
def bold(fn):
return lambda: "<b>" + fn() + "</b>"
def italic(fn):
return lambda: "<i>" + fn() + "</i>"
@bold
@italic
def text():
return "hi"
text() # "<b><i>hi</i></b>" — bold is outer, runs around italic
So the closest decorator is applied first but its logic runs innermost.
Order matters whenever decorators have side effects or transform results — e.g.
put @staticmethod outermost, or @app.route above @login_required so auth
runs before the view.
@dec above a definition is just func = dec(func) — the decorator is
called with the function and its return value rebinds the name. That's the
whole mechanism; everything else is convention.
@log
def greet(): ...
# identical to:
def greet(): ...
greet = log(greet)
Rule of thumb: read @dec as "replace the name with dec(name)".
The decorated function reports the wrapper's __name__, __doc__, and
signature instead of the original's — breaking help(), debuggers,
introspection, and some frameworks (e.g. ones that read function names for
routing). @wraps(fn) copies that metadata over.
from functools import wraps
def log(fn):
@wraps(fn) # without this, greet.__name__ == 'inner'
def inner(*a, **k): return fn(*a, **k)
return inner
Rule of thumb: always wrap the inner function with @wraps(fn) so the
decorated function keeps its identity.
Make the argument optional and detect whether the first positional is the
function itself. If called as @dec the function is passed directly; if
@dec(...) it isn't. Use a keyword-only config plus a func=None check.
from functools import wraps, partial
def retry(func=None, *, times=3):
if func is None:
return partial(retry, times=times) # called as @retry(times=5)
@wraps(func)
def inner(*a, **k):
for _ in range(times):
try: return func(*a, **k)
except Exception: pass
return inner
Rule of thumb: return partial(dec, **opts) when the function slot is empty,
so both @dec and @dec(...) work.
Yes — a class decorator receives the class and returns a (usually modified)
class. It's used to register classes, inject methods/attributes, or wrap them.
@dataclass is the canonical example. It runs after the class body
executes.
registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class Plugin: ...
Rule of thumb: use class decorators to augment or register a class without subclassing or a metaclass.
They apply bottom-up at definition time (the nearest decorator wraps
first), but execute top-down at call time (the outermost runs first).
@a @b def f means a(b(f)).
@bold # outer: runs second when called, wraps last
@italic # inner: runs first when called, wraps first
def text(): return "hi"
# text = bold(italic(text))
Rule of thumb: read the stack as nested calls — bottom decorator is innermost, top is outermost.
@property turns a method into a managed attribute with getter
semantics; @x.setter and @x.deleter add write/delete behavior. It's
a descriptor that runs your method on attribute access, enabling computed or
validated attributes without changing call sites.
class C:
@property
def value(self): return self._v
@value.setter
def value(self, v):
if v < 0: raise ValueError
self._v = v
c = C(); c.value = 5 # calls the setter
Rule of thumb: use @property to expose computed/validated attributes that
look like plain attribute access.
The decorator call (and any setup outside the wrapper) runs once, at import/definition time. Only the inner wrapper runs on each call. So registration, validation, or logging placed in the decorator body executes when the module loads, not per call.
def trace(fn):
print("decorating", fn.__name__) # runs at import
def inner(*a, **k):
print("calling") # runs each call
return fn(*a, **k)
return inner
Rule of thumb: put per-call logic in the inner wrapper; one-time setup goes in the decorator body.
@staticmethod makes a method that takes no implicit first arg — just
a namespaced plain function. @classmethod passes the class as cls,
enabling alternative constructors and class-level behavior. Both are
descriptors applied via decorator syntax.
class Date:
@classmethod
def today(cls): # cls = Date (or a subclass)
return cls(...)
@staticmethod
def is_leap(y): # no self/cls
return y % 4 == 0
Rule of thumb: classmethod for alternative constructors/factory methods;
staticmethod for utility functions logically grouped under a class.
Store it in the enclosing closure (a captured variable) or on the
wrapper function's attributes. A class-based decorator can keep state on
self. Use this for counters, caches, or rate limiters.
from functools import wraps
def count_calls(fn):
@wraps(fn)
def inner(*a, **k):
inner.calls += 1
return fn(*a, **k)
inner.calls = 0
return inner
Rule of thumb: keep decorator state in the closure or on the wrapper/self,
not in globals.
It's factory → decorator → wrapper: the outermost function takes the
arguments and returns a decorator; that decorator takes the function
and returns the wrapper that runs at call time. Three nested defs.
from functools import wraps
def repeat(n): # 1) takes args
def decorator(fn): # 2) takes the function
@wraps(fn)
def wrapper(*a, **k): # 3) runs at call time
for _ in range(n):
r = fn(*a, **k)
return r
return wrapper
return decorator
@repeat(3)
def hi(): print("hi")
Rule of thumb: a parametrized decorator needs three layers — remember args-layer, function-layer, call-layer.
More Functions interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.