datarekha
Python Hard Asked at GoogleAsked at StripeAsked at AirbnbAsked at Netflix

How do you write a decorator that accepts its own arguments?

The short answer

You add one more layer of nesting: a factory function that accepts the decorator's arguments and returns the actual decorator. The @syntax then calls the factory first, and the result decorates the function.

How to think about it

A plain decorator has the shape decorator(func) -> func. To let it take its own arguments, you add one more layer: a factory that receives the arguments and returns a decorator. So @retry(max_attempts=3) isn’t one step — it’s two. Python first calls retry(max_attempts=3), gets a decorator back, and then applies that to your function.

This is really a closures question wearing a decorator costume. What the interviewer is grading is whether you can keep the three levels straight and remember functools.wraps.

The three levels

retry(max_attempts, delay)            ← factory (level 1), runs at decoration time
  └── decorator(func)                 ← the actual decorator (level 2)
        └── wrapper(*args, **kwargs)   ← the replacement function (level 3), runs on every call

The factory runs once, when the @ line is reached. The wrapper runs every time the decorated function is called. Each inner function closes over the scope above it — the wrapper remembers func, the decorator remembers max_attempts and delay.

A worked example

import functools
import time

def retry(max_attempts=3, delay=0.0):
    """Factory: returns a decorator that retries on exception."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exc = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as exc:
                    last_exc = exc
                    print(f"  attempt {attempt}/{max_attempts} failed: {exc}")
                    if delay > 0:
                        time.sleep(delay)
            raise last_exc
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.0)      # = flaky_operation = retry(...)(flaky_operation)
def flaky_operation(succeed_on):
    flaky_operation._calls = getattr(flaky_operation, "_calls", 0) + 1
    if flaky_operation._calls < succeed_on:
        raise ValueError(f"Not ready yet (call {flaky_operation._calls})")
    return f"Success on call {flaky_operation._calls}!"

print("Result:", flaky_operation(succeed_on=3))
print("Function name:", flaky_operation.__name__)   # preserved by functools.wraps

# Make the parentheses optional, so both @log and @log(prefix=...) work
def log(_func=None, *, prefix="LOG"):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{prefix}] calling {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    if _func is not None:       # used as @log  (no parens)
        return decorator(_func)
    return decorator            # used as @log() or @log(prefix="DEBUG")

@log
def add(a, b):
    return a + b

@log(prefix="DEBUG")
def mul(a, b):
    return a * b

print(add(2, 3))
print(mul(4, 5))
  attempt 1/3 failed: Not ready yet (call 1)
  attempt 2/3 failed: Not ready yet (call 2)
Result: Success on call 3!
Function name: flaky_operation
[LOG] calling add
5
[DEBUG] calling mul
20

Watch the retries: the first two calls raise and are caught, the third succeeds, and flaky_operation.__name__ still reads flaky_operation thanks to wraps. The log factory then shows the polished trick — the _func=None guard lets the same decorator be used with or without parentheses.

The idea underneath

It’s closures all the way down. The factory call returns a decorator that closes over max_attempts and delay; that decorator returns a wrapper that closes over func. Three nested scopes, each capturing the one above it. See it that way and the pattern stops being fiddly and turns mechanical.

Learn it properly Decorators

Keep practising

All Python questions

Explore further

Skip to content