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.

A decorator with arguments is actually a decorator factory: a function that returns a decorator.

Factory Pattern

name
factory.py
Replay: real traced execution (multi-file project)
# 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 = "Alice"
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 = "Maya"
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 = "Jordan"
greet(name)

  1. def repeat(times):

    6def repeat(times3):7    """Decorator that repeats function execution."""89    def decorator(func):10        @wraps(func)11        def wrapper(*args, **kwargs):12            for _ in range(times):13                result = func(*args, **kwargs)14            return result1516        return wrapper1718    return decorator<function repeat.<locals>.decorator at ⟨addr A⟩>
  2. def decorator(func):

    9def decorator(func⟨function greet B⟩):10    @wraps(func)11    def wrapper(*args, **kwargs):12        for _ in range(times):13            result = func(*args, **kwargs)14        return result1516    return wrapper⟨function greet C⟩
  3. name ← Alice

    28name→ Alice = "Alice"29#@name="Maya", "Jordan"30greet(nameAlice)
  4. def wrapper(*args, **kwargs):

    10@wraps(func)11def wrapper(*args('Alice',), **kwargs):12    for _ in range(times):13        result = func(*args, **kwargs)
  5. for _ in range(times):

    pass 1 of 3
    11def wrapper(*args, **kwargs):12    for _0 in range(times3):13        result = func(*args('Alice',), **kwargs{})14    return result
    All 3 passes — pass 1 is the card above
    pass_
    10
    21
    32
  6. def greet(name):

    pass 1 of 3
    22@repeat(times=3)23def greet(nameAlice):24    print(f"hello, {nameAlice}")25    return "ok"
    outputhello, Alice
  7. result ← ok

    12for _ in range(times):13    result→ ok = func(*args('Alice',), **kwargs{})14return result
  8. result ← ok

    12for _ in range(times):13    result→ ok = func(*args('Alice',), **kwargs{})14return result
  9. result ← ok

    12for _ in range(times):13    result→ ok = func(*args('Alice',), **kwargs{})14return resultok
  10. greet(name)

    29#@name="Maya", "Jordan"30greet(nameAlice)
  1. def repeat(times):

    6def repeat(times3):7    """Decorator that repeats function execution."""89    def decorator(func):10        @wraps(func)11        def wrapper(*args, **kwargs):12            for _ in range(times):13                result = func(*args, **kwargs)14            return result1516        return wrapper1718    return decorator<function repeat.<locals>.decorator at ⟨addr A⟩>
  2. def decorator(func):

    9def decorator(func⟨function greet B⟩):10    @wraps(func)11    def wrapper(*args, **kwargs):12        for _ in range(times):13            result = func(*args, **kwargs)14        return result1516    return wrapper⟨function greet C⟩
  3. name ← Maya

    28name→ Maya = "Maya"29greet(nameMaya)
  4. def wrapper(*args, **kwargs):

    10@wraps(func)11def wrapper(*args('Maya',), **kwargs):12    for _ in range(times):13        result = func(*args, **kwargs)
  5. for _ in range(times):

    pass 1 of 3
    11def wrapper(*args, **kwargs):12    for _0 in range(times3):13        result = func(*args('Maya',), **kwargs{})14    return result
    All 3 passes — pass 1 is the card above
    pass_
    10
    21
    32
  6. def greet(name):

    pass 1 of 3
    22@repeat(times=3)23def greet(nameMaya):24    print(f"hello, {nameMaya}")25    return "ok"
    outputhello, Maya
  7. result ← ok

    12for _ in range(times):13    result→ ok = func(*args('Maya',), **kwargs{})14return result
  8. result ← ok

    12for _ in range(times):13    result→ ok = func(*args('Maya',), **kwargs{})14return result
  9. result ← ok

    12for _ in range(times):13    result→ ok = func(*args('Maya',), **kwargs{})14return resultok
  10. greet(name)

    28name = "Maya"29greet(nameMaya)
  1. def repeat(times):

    6def repeat(times3):7    """Decorator that repeats function execution."""89    def decorator(func):10        @wraps(func)11        def wrapper(*args, **kwargs):12            for _ in range(times):13                result = func(*args, **kwargs)14            return result1516        return wrapper1718    return decorator<function repeat.<locals>.decorator at ⟨addr A⟩>
  2. def decorator(func):

    9def decorator(func⟨function greet B⟩):10    @wraps(func)11    def wrapper(*args, **kwargs):12        for _ in range(times):13            result = func(*args, **kwargs)14        return result1516    return wrapper⟨function greet C⟩
  3. name ← Jordan

    28name→ Jordan = "Jordan"29greet(nameJordan)
  4. def wrapper(*args, **kwargs):

    10@wraps(func)11def wrapper(*args('Jordan',), **kwargs):12    for _ in range(times):13        result = func(*args, **kwargs)
  5. for _ in range(times):

    pass 1 of 3
    11def wrapper(*args, **kwargs):12    for _0 in range(times3):13        result = func(*args('Jordan',), **kwargs{})14    return result
    All 3 passes — pass 1 is the card above
    pass_
    10
    21
    32
  6. def greet(name):

    pass 1 of 3
    22@repeat(times=3)23def greet(nameJordan):24    print(f"hello, {nameJordan}")25    return "ok"
    outputhello, Jordan
  7. result ← ok

    12for _ in range(times):13    result→ ok = func(*args('Jordan',), **kwargs{})14return result
  8. result ← ok

    12for _ in range(times):13    result→ ok = func(*args('Jordan',), **kwargs{})14return result
  9. result ← ok

    12for _ in range(times):13    result→ ok = func(*args('Jordan',), **kwargs{})14return resultok
  10. greet(name)

    28name = "Jordan"29greet(nameJordan)
decorator factory - a function that takes parameters and returns a decorator, enabling configurable decoration

How It Works

This is equivalent to:

my_func = decorator_with_args(val1, val2)(my_func)

Parametrized Decorators

parametrized.py
Replay: real traced execution (multi-file project)
# 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()

  1. def log_with_prefix(prefix):

    pass 1 of 2
    6def log_with_prefix(prefix[INFO]):7    def decorator(func):8        @wraps(func)9        def wrapper(*args, **kwargs):10            print(f"{prefix}: calling {func.__name__}")11            return func(*args, **kwargs)1213        return wrapper1415    return decorator<function log_with_prefix.<locals>.decorator at ⟨addr A⟩>
  2. def decorator(func):

    pass 1 of 2
    6def log_with_prefix(prefix):7    def decorator(func⟨function task_a B⟩):8        @wraps(func)9        def wrapper(*args, **kwargs):10            print(f"{prefix}: calling {func.__name__}")11            return func(*args, **kwargs)1213        return wrapper⟨function task_a C⟩
  3. def log_with_prefix(prefix):

    pass 2 of 2
    6def log_with_prefix(prefix[DEBUG]):7    def decorator(func):8        @wraps(func)9        def wrapper(*args, **kwargs):10            print(f"{prefix}: calling {func.__name__}")11            return func(*args, **kwargs)1213        return wrapper1415    return decorator<function log_with_prefix.<locals>.decorator at ⟨addr A⟩>
  4. def decorator(func):

    pass 2 of 2
    6def log_with_prefix(prefix):7    def decorator(func⟨function task_b D⟩):8        @wraps(func)9        def wrapper(*args, **kwargs):10            print(f"{prefix}: calling {func.__name__}")11            return func(*args, **kwargs)1213        return wrapper⟨function task_b E⟩
  5. task_a()

    28task_a()29task_b()
  6. def wrapper(*args, **kwargs):

    pass 1 of 2
    8@wraps(func)9def wrapper(*args(), **kwargs):10    print(f"{prefix[INFO]}: calling {func.__name__task_a}")11    return func(*args(), **kwargs{})
    output[INFO]: calling task_a
  7. task_a()

    28task_a()29task_b()
  8. def wrapper(*args, **kwargs):

    pass 2 of 2
    8@wraps(func)9def wrapper(*args(), **kwargs):10    print(f"{prefix[DEBUG]}: calling {func.__name__task_b}")11    return func(*args(), **kwargs{})
    output[DEBUG]: calling task_b
  9. task_b()

    28task_a()29task_b()

Multiple Parameters

multiple_params.py
Replay: real traced execution (multi-file project)
# Multiple parameters

from functools import wraps


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

    def decorator(func):
        calls = []

        @wraps(func)
        def wrapper(*args, **kwargs):
            now = 1000.0
            # 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

  1. def throttle(max_calls, period_seconds):

    6def throttle(max_calls2, period_seconds1):7    """Allow max_calls within period_seconds."""89    def decorator(func):10        calls = []1112        @wraps(func)13        def wrapper(*args, **kwargs):14            now = 1000.015            # Remove old calls16            while calls and calls[0] < now - period_seconds:17                calls.pop(0)1819            if len(calls) >= max_calls:20                raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")2122            calls.append(now)23            return func(*args, **kwargs)2425        return wrapper2627    return decorator<function throttle.<locals>.decorator at ⟨addr A⟩>
  2. calls ← []

    9def decorator(func⟨function api_call B⟩):10    calls→ [] = []1112    @wraps(func)13    def wrapper(*args, **kwargs):14        now = 1000.015        # Remove old calls16        while calls and calls[0] < now - period_seconds:17            calls.pop(0)1819        if len(calls) >= max_calls:20            raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")2122        calls.append(now)23        return func(*args, **kwargs)2425    return wrapper⟨function api_call C⟩
  3. api_call()

    35api_call()36api_call()
  4. now ← 1000.0, calls ← [1000.0]

    pass 1 of 2
    12@wraps(func)13def wrapper(*args(), **kwargs):14    now→ 1000.0 = 1000.015    # Remove old calls16    while calls and calls[0] < now - period_seconds:17        calls.pop(0)1819    if len(calls) >= max_calls:20        raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")2122    calls→ [1000.0].append(now1000.0)23    return func(*args(), **kwargs{})
  5. def api_call():

    pass 1 of 2
    30@throttle(max_calls=2, period_seconds=1)31def api_call():32    print("API called")
    outputAPI called
  6. api_call()

    35api_call()36api_call()37# Third call would raise if done quickly
  7. now ← 1000.0, calls ← [1000.0, 1000.0]

    pass 2 of 2
    12@wraps(func)13def wrapper(*args(), **kwargs):14    now→ 1000.0 = 1000.015    # Remove old calls16    while calls and calls[0] < now - period_seconds:17        calls.pop(0)1819    if len(calls) >= max_calls:20        raise RuntimeError(f"rate limit: max {max_calls} calls per {period_seconds}s")2122    calls→ [1000.0, 1000.0].append(now1000.0)23    return func(*args(), **kwargs{})
  8. def api_call():

    pass 2 of 2
    30@throttle(max_calls=2, period_seconds=1)31def api_call():32    print("API called")
    outputAPI called
  9. api_call()

    35api_call()36api_call()37# Third call would raise if done quickly

Optional Arguments

optional_args.py
Replay: real traced execution (multi-file project)
# 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()

  1. def optional_prefix(arg=None):

    pass 1 of 2
    6def optional_prefix(arg[WARN]=NoneNone):7    """Decorator that can be used with or without arguments."""89    def decorator(func):10        prefix = arg if arg is not None else "[LOG]"1112        @wraps(func)13        def wrapper(*args, **kwargs):14            print(f"{prefix}: {func.__name__}")15            return func(*args, **kwargs)1617        return wrapper1819    # If called without parens, arg is the function itself20    if callable(arg):21        func = arg22        arg = None23        return decorator(func)2425    return decorator<function optional_prefix.<locals>.decorator at ⟨addr A⟩>
  2. prefix ← [WARN]

    pass 1 of 2
    9def decorator(func⟨function task_a B⟩):10    prefix→ [WARN] = arg[WARN] if arg is not None else "[LOG]"1112    @wraps(func)13    def wrapper(*args, **kwargs):14        print(f"{prefix}: {func.__name__}")15        return func(*args, **kwargs)1617    return wrapper⟨function task_a C⟩
  3. def optional_prefix(arg=None):

    pass 2 of 2
    6def optional_prefix(arg⟨function task_b A⟩=NoneNone):7    """Decorator that can be used with or without arguments."""
  4. func ← ⟨function task_b A⟩, arg ← None

    19# If called without parens, arg is the function itself20if callable(arg⟨function task_b A⟩):21    func→ ⟨function task_b A⟩ = arg⟨function task_b A⟩22    arg→ None = None23    return decorator(func⟨function task_b A⟩)
  5. prefix ← [LOG]

    pass 2 of 2
    9def decorator(func⟨function task_b A⟩):10    prefix→ [LOG] = argNone if arg is not None else "[LOG]"1112    @wraps(func)13    def wrapper(*args, **kwargs):14        print(f"{prefix}: {func.__name__}")15        return func(*args, **kwargs)1617    return wrapper⟨function task_b D⟩
  6. task_a()

    40task_a()41task_b()
  7. def wrapper(*args, **kwargs):

    pass 1 of 2
    12@wraps(func)13def wrapper(*args(), **kwargs):14    print(f"{prefix[WARN]}: {func.__name__task_a}")15    return func(*args(), **kwargs{})
    output[WARN]: task_a
  8. task_a()

    40task_a()41task_b()
  9. def wrapper(*args, **kwargs):

    pass 2 of 2
    12@wraps(func)13def wrapper(*args(), **kwargs):14    print(f"{prefix[LOG]}: {func.__name__task_b}")15    return func(*args(), **kwargs{})
    output[LOG]: task_b
  10. task_b()

    40task_a()41task_b()
optional decorator args - making decorator parameters optional with defaults

Class-Based Approach

An alternative using classes:

class_based.py
Replay: real traced execution (multi-file project)
# 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()

  1. self.max_calls ← 3

    6class CountCalls:7    def __init__(self⟨CountCalls A⟩, max_calls3):8        self.max_calls→ 3 = max_calls3
  2. count ← {'value': 0}

    10def __call__(self⟨CountCalls A⟩, func⟨function task B⟩):11    count→ {'value': 0} = {"value": 0}1213    @wraps(func)14    def wrapper(*args, **kwargs):15        count["value"] += 116        if count["value"] > self.max_calls:17            raise RuntimeError(f"exceeded max calls: {self.max_calls}")18        print(f"call {count['value']}/{self.max_calls}")19        return func(*args, **kwargs)2021    return wrapper⟨function task C⟩
  3. task()

    29task()30task()
  4. count[”value”] ← 1

    pass 1 of 3
    13@wraps(func)14def wrapper(*args(), **kwargs):15    count["value"]→ 1 += 116    if count["value"] > self.max_calls:17        raise RuntimeError(f"exceeded max calls: {self.max_calls}")18    print(f"call {count['value']1}/{self.max_calls3}")19    return func(*args(), **kwargs{})
    outputcall 1/3
    All 3 passes — pass 1 is the card above
    passcount[’value’]count[”value”]
    110 1
    221 2
    332 3
  5. task()

    29task()30task()31task()
  6. task()

    29task()30task()31task()
  7. task()

    30task()31task()

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

Exercise: practical.py

Build a rate-limiting decorator with configurable limits