Decorator Factories with Arguments

RoadmapsPython

Overview

A decorator factory is a function that returns a decorator. Standard decorators only accept one argument, which is the function being decorated. To pass custom arguments, you create an outer function that receives those arguments and returns the actual decorator. @retry(max_retries=3) def fetch_data(): pass This architecture requires exactly three nested levels of functions: - Factory: Receives the configuration arguments. - Decorator: Receives the target function. - Wrapper: Receives the target function's runtime arguments. Think of the factory as a machine that configures and builds a custom decorator based on the specs you give it. A sharp edge arises when you forget the parentheses on a factory, or try to support both @retry and @retry(max=3). If you omit the parentheses, Python passes the target function as the first configuration argument. @retry # Passes fetch_data into max_retries! def fetch_data(): pass Supporting both syntaxes requires checking if the first argument is callable(), which adds significant complexity to your factory.

It enables configurable decorators. Instead of writing separate decorators for different behaviors, you write one configurable factory.

Where used: Flask routing (@app.route('/login')), FastAPI endpoints (@app.get('/items')), Pytest parametrization (@pytest.mark.parametrize)

Why learn this

Code walkthrough

def multiply_result_by(factor):
  def decorator(func):
    def wrapper(*args, **kwargs):
      return func(*args, **kwargs) * factor
    return wrapper
  return decorator

@multiply_result_by(1.2)
def get_base_price(item_id):
  return 50.0

print(get_base_price('item_42'))

Focus: return func(*args, **kwargs) * factor

Aha moment

def route(path):
  print(f'Registering {path}')
  def decorator(func):
    print('Decorator attached')
    def wrapper(*args, **kwargs):
      print('Handling request')
      return func(*args, **kwargs)
    return wrapper
  return decorator

@route('/home')
def home_view():
  return 'Welcome'

print('App started')

Prediction: What is the exact order of the printed lines when this script runs?

Common guess: App started Registering /home Decorator attached Handling request

The factory and decorator run at module load time to register the route before the app even starts. The wrapper only runs when a client actually makes a request, which doesn't happen here.

Common mistakes

Glossary

decorator
A special function that wraps another function to modify or enhance its behavior without changing its code, e.g. @retry(max_retries=3).
wrapper
An inner function that surrounds the original function, allowing it to run extra code before or after the original function runs, e.g. def wrapper(*args): return func(*args).

Recall questions

Understanding checks

What happens when you run this code?

A `TypeError` is raised when `say_hello()` is called: `say_hello` is now bound to `decorator` (since `@prefix` passed the function itself as `p`), and calling `decorator()` with no arguments fails because `decorator` requires the `func` parameter.

The syntax @prefix means say_hello = prefix(say_hello). prefix is a factory expecting the string p, but it receives the function say_hello. It returns decorator, meaning say_hello is now the decorator function itself. Decorator factories must be called: @prefix('Hello').

When defining a decorator factory, why do we need three levels of nested functions instead of two?

Because the factory needs to take the configuration arguments, return a decorator that takes the target function, which in turn returns the wrapper that takes the runtime arguments.

Standard decorators take the function automatically. A factory function isn't a decorator itself; it's a function that returns a decorator. Thus, Level 1: take config arguments, Level 2: take target function, Level 3: take target function's arguments.

Practice tasks

Add Configuration to a Decorator

We have a basic `@repeat` decorator that repeats a function 3 times. Modify it into a decorator factory `repeat(times)` so we can configure the number of repeats per function.

Challenge

Rate Limiting Factory

Create a decorator factory `rate_limit` that accepts an integer `max_requests`. It should decorate a function representing an API endpoint. If the endpoint is called more than `max_requests` times, the wrapper should return the string `'Rate limit exceeded'`. Otherwise, it should increment a counter and return the function's result. You can use an attribute on the wrapper itself to store the call count.

Continue learning

Previous: Function Decorators

Previous: Closures

Previous: functools.wraps and preserving metadata

Return to Python Roadmap