← Back to Python

Decorator Factory

PythonMidpython

The Question

What is a decorator factory and when would you use one?

What a Strong Answer Covers

  • three nested layers
  • "outer=args
  • middle=function
  • inner=wrapper

Senior-Level Answer

A decorator is a function that takes a function and returns a modified function. A **decorator factory** is a function that takes arguments and *returns* a decorator. It adds one layer of nesting to allow the decorator to be parameterized.

Without a factory, a decorator is applied as `@my_decorator`. To pass arguments, you'd want `@my_decorator(arg1, arg2)`. This syntax requires `my_decorator(arg1, arg2)` to be called first, returning a decorator—that's a factory.

The structure:

```python def repeat(n): # factory — takes arguments def decorator(func): # actual decorator — takes function def wrapper(*args, **kwargs): for _ in range(n): func(*args, **kwargs) return wrapper return decorator

@repeat(3) def greet(name): print(f'Hello, {name}') ```

Here `repeat(3)` is called at decoration time, returning `decorator`, which then wraps `greet`. The `n=3` is captured in the closure of `wrapper`.

The extra function level (factory → decorator → wrapper) is always the pattern. The factory receives configuration; the decorator receives the function; the wrapper receives call-time arguments.

When to use a decorator factory: any time you want the decorator's behavior to be configurable per use-site—retry count (`@retry(max_attempts=3)`), cache TTL (`@cache(ttl=60)`), required permissions (`@require_role('admin')`), rate limit per endpoint (`@rate_limit(calls=100, period=60)`).

`functools.wraps` should always be applied to the wrapper to preserve the original function's `__name__`, `__doc__`, and `__wrapped__` attribute—without it, introspection and stack traces show `wrapper` instead of the original function name, making debugging painful.

A common pattern is making the factory optional (supporting both `@my_decorator` and `@my_decorator(arg)`), which requires checking whether the argument to the factory is callable—but this adds complexity and is usually not worth it outside library code.

What Separates a 2/3 from a 3/3

2/3 — Passing but Incomplete

Explains the three-level nesting (factory → decorator → wrapper), shows how arguments are captured via closure, gives a practical use case.

3/3 — Strong Answer

Explains why the extra nesting is needed for argument passing, demonstrates functools.wraps usage and explains why it matters, gives multiple real use cases (retry, cache, auth), and discusses the optional-argument factory pattern.

Common Mistakes

  • Confusing a decorator with a decorator factory — not understanding the extra nesting level
  • Forgetting functools.wraps on the wrapper — breaks introspection and stack traces
  • Not explaining the closure mechanism — 'n' in the repeat example is captured, not global
  • Thinking decorator factories are only for library/framework code — they're common in application code too

Follow-Up Questions

  • Why must functools.wraps be applied to the wrapper function, not the decorator? — functools.wraps copies __name__, __doc__, __annotations__ from the wrapped function to the wrapper. It must be applied to the innermost wrapper that replaces the original function.
  • How would you implement a @retry(max_attempts=3, exceptions=(TimeoutError,)) decorator factory? — Factory takes max_attempts and exceptions. Decorator takes func. Wrapper calls func in a loop, catching specified exceptions, re-raising on final attempt, returning result on success.
  • What is the difference between applying @decorator and @decorator()? — @decorator passes the function directly to decorator. @decorator() calls decorator() first (with no args) and expects it to return a decorator. They have different signatures and cannot be used interchangeably.
  • How would you make a decorator that works both with and without arguments? — Check if the first argument to the factory is callable (i.e., the function was passed directly). If yes, apply the decorator with defaults. If no, treat it as configuration and return a decorator. Use inspect.isfunction or a sentinel default argument.

Related Questions

  • GIL — What It Is and What It Protects
  • ThreadPoolExecutor & ProcessPoolExecutor
  • Decorators — Under the Hood
  • Generators & yield
  • Context Managers

Can You Explain This Cold?

Reading the answer is step one. Explaining it unprompted — under interview pressure — is what actually matters. Get AI-graded feedback on your answer with follow-up probes on your weak points.

Get Graded — Free Assessment
GrindQuestionsAITechnical interview assessment
TermsPrivacyAbout