A simple @retry decorator is useful, but @retry(max_attempts=3) is more flexible. Decorator factories let you configure decorator behavior with parameters, turning a single decorator into a family of related behaviors.

Class-Based Approach

An alternative using classes:

factory.py
# Decorator factory pattern

from functools import wraps


def repeat(times):
    """Decorator that repeats function execution."""

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result

        return wrapper

    return decorator


# Use decorator with argument
@repeat(times=3)
def greet(name):
    print(f"hello, {name}")
    return "ok"


name = 
greet(name)

# Decorator factory pattern

from functools import wraps


def repeat(times):
    """Decorator that repeats function execution."""

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result

        return wrapper

    return decorator


# Use decorator with argument
@repeat(times=3)
def greet(name):
    print(f"hello, {name}")
    return "ok"


name = 
greet(name)

# Decorator factory pattern

from functools import wraps


def repeat(times):
    """Decorator that repeats function execution."""

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result

        return wrapper

    return decorator


# Use decorator with argument
@repeat(times=3)
def greet(name):
    print(f"hello, {name}")
    return "ok"


name = 
greet(name)

parametrized.py
# Parametrized decorator

from functools import wraps


def log_with_prefix(prefix):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{prefix}: calling {func.__name__}")
            return func(*args, **kwargs)

        return wrapper

    return decorator


@log_with_prefix("[INFO]")
def task_a():
    return "done"


@log_with_prefix("[DEBUG]")
def task_b():
    return "ok"


task_a()
task_b()

multiple_params.py
# Multiple parameters

from functools import wraps
import time


def throttle(max_calls, period_seconds):
    """Allow max_calls within period_seconds."""

    def decorator(func):
        calls = []

        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            # Remove old calls
            while calls and calls[0] < now - period_seconds:
                calls.pop(0)

            if len(calls) >= max_calls:
                raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")

            calls.append(now)
            return func(*args, **kwargs)

        return wrapper

    return decorator


@throttle(max_calls=2, period_seconds=1)
def api_call():
    print("API called")


api_call()
api_call()
# Third call would raise if done quickly

optional_args.py
# Optional decorator arguments

from functools import wraps


def optional_prefix(arg=None):
    """Decorator that can be used with or without arguments."""

    def decorator(func):
        prefix = arg if arg is not None else "[LOG]"

        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{prefix}: {func.__name__}")
            return func(*args, **kwargs)

        return wrapper

    # If called without parens, arg is the function itself
    if callable(arg):
        func = arg
        arg = None
        return decorator(func)

    return decorator


# With argument
@optional_prefix("[WARN]")
def task_a():
    pass


# Without argument
@optional_prefix
def task_b():
    pass


task_a()
task_b()

class_based.py
# Class-based decorator with args

from functools import wraps


class CountCalls:
    def __init__(self, max_calls):
        self.max_calls = max_calls

    def __call__(self, func):
        count = {"value": 0}

        @wraps(func)
        def wrapper(*args, **kwargs):
            count["value"] += 1
            if count["value"] > self.max_calls:
                raise RuntimeError(f"exceeded max calls: {self.max_calls}")
            print(f"call {count['value']}/{self.max_calls}")
            return func(*args, **kwargs)

        return wrapper


@CountCalls(max_calls=3)
def task():
    return "ok"


task()
task()
task()

Common Use Cases

  • Repeat n times
  • Retry up to max_attempts
  • Cache with max_size
  • Throttle to max_calls_per_second
  • Validate with custom rules
decorator factory - a function that takes parameters and returns a decorator, enabling configurable decoration
optional decorator args - making decorator parameters optional with defaults

Exercise: practical.py

Build a rate-limiting decorator with configurable limits