How do decorators work, and how do you write one that accepts arguments?

7 minintermediatefunctionsdecorators

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.