How do decorators work, and how do you write one that accepts arguments?
Quick Answer
A decorator is a function that takes a function and returns a (usually wrapped) function; `@decorator` above a `def` is syntax sugar for `func = decorator(func)`. A **decorator that takes arguments** needs an extra level of nesting: the outer function takes the arguments and returns the actual decorator, which takes `func` and returns the wrapper.
Detailed Answer
What @decorator actually does
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@shout
def greet(name):
return f"hello, {name}"
# equivalent to:
# def greet(name): ...
# greet = shout(greet)
greet("ada") # 'HELLO, ADA'
@shout just rebinds the name greet to whatever shout(greet) returns
— here, the wrapper closure, which calls the original greet and
transforms its result. func is captured in wrapper's closure, so it's
still callable even though greet itself has been reassigned.
A decorator that takes its own arguments
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(times=3)
def roll_die():
import random
return random.randint(1, 6)
# equivalent to:
# roll_die = repeat(times=3)(roll_die)
roll_die() # [4, 1, 6] (three rolls)
Three levels: repeat(times) returns decorator; decorator(func)
returns wrapper; calling wrapper(...) finally runs the original
function. @repeat(times=3) evaluates repeat(times=3) first (producing
decorator), then applies that to roll_die — this is why a
parameterized decorator always needs parentheses even with no arguments
inside them (@repeat()), while a plain decorator never does (@shout,
not @shout()).
Stacking multiple decorators
@decorator_a
@decorator_b
def f(): ...
# equivalent to: f = decorator_a(decorator_b(f))
Decorators apply bottom-up: the one closest to the function runs first (wraps first), so the outermost decorator in the source is the outermost wrapper at call time.
Class-based decorators
class CountCalls:
def __init__(self, func):
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
return self.func(*args, **kwargs)
@CountCalls
def greet():
return "hi"
greet(); greet()
greet.calls # 2
Any object with __call__ can be a decorator — a class-based decorator
is useful when the wrapper needs to hold non-trivial state (here, a call
counter) rather than a single closed-over variable.
Interview-ready summary: @decorator is sugar for func = decorator(func); a decorator that itself takes arguments needs an extra
function layer (args -> decorator -> wrapper). Decorators stack bottom-up,
and anything callable — including a class implementing __call__ — can
serve as one.