How do you write a decorator that accepts its own arguments?
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 signature decorator(func) -> func. To give it its own arguments you need one more layer of nesting — a factory that accepts the arguments and returns the decorator. The @ syntax then calls the factory first, and the result is the actual decorator.
What’s really being tested
This is really a closures question in disguise. Interviewers want to see you understand that @retry(max_attempts=3) is just call_api = retry(max_attempts=3)(call_api) — two separate calls. Getting the three levels of nesting right, and using functools.wraps to preserve the original function’s metadata, are the things they’re grading you on.
Step 1 — Understand the three levels
retry(max_attempts, delay) ← factory (level 1)
└── decorator(func) ← actual decorator (level 2)
└── wrapper(*args, **kwargs) ← replacement function (level 3)
The factory is called at decoration time (before the function ever runs). The wrapper is called every time the decorated function is invoked.
Step 2 — Build it up piece by piece
Start with a plain decorator, then wrap it in the factory. The arguments become closed-over variables inside the decorator and wrapper.
Step 3 — Always use functools.wraps
Without @functools.wraps(func), the wrapper will have the wrong __name__, __doc__, and __module__. This breaks help(), logging, and any introspection tool.
The key insight — it’s just closures
The factory call returns the decorator, which closes over max_attempts and delay. The decorator returns the wrapper, which closes over func. Three nested closures, each capturing the scope above it. Once you see it that way, the pattern becomes mechanical.