Decorators that take arguments (parameterized decorators) are a natural extension of basic decorators in Python — they let you customize decorator behavior at decoration time by passing parameters (e.g., repeat count, log level, retry attempts, cache size). This is achieved with a decorator factory: an outer function that accepts the parameters and returns the actual decorator, which then wraps the target function. In 2026, parameterized decorators are everywhere — powering configurable logging, timing with thresholds, caching with maxsize, retry policies, rate limiting, validation rules, and more in FastAPI, Flask, Celery, pandas/Polars pipelines, ML training loops, and production systems. They make decorators flexible, reusable, and declarative — turning @retry(3) or @cache(maxsize=128) into simple, readable syntax.
Here’s a complete, practical guide to decorators that take arguments in Python: factory pattern, syntax and mechanics, preserving metadata with @wraps, real-world examples (repeat, retry, cache), and modern best practices with type hints, functools, logging, and pandas/Polars integration.
Basic parameterized decorator — outer factory takes arguments, returns inner decorator that wraps the function.
def repeat(num_times: int):
"""Decorator factory: repeats the function num_times times."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result # return last result (or collect all if needed)
return wrapper
return decorator
@repeat(num_times=3)
def say_hi(name: str) -> None:
"""Print a greeting."""
print(f"Hi, {name}!")
say_hi("Alice")
# Hi, Alice!
# Hi, Alice!
# Hi, Alice!
Preserving metadata — always use @functools.wraps inside the decorator — otherwise the wrapped function loses name, docstring, type hints.
from functools import wraps
def repeat(num_times: int):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(num_times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(2)
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
print(greet.__name__) # greet (not wrapper)
print(greet.__doc__) # Greet someone.
print(greet("Bob")) # Hello, Bob! (twice)
Real-world pattern: retry decorator for flaky pandas/Polars operations — retry on transient errors (e.g., network, file access).
import time
from functools import wraps
import polars as pl
def retry(max_attempts: int = 3, delay: float = 1.0):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed: {e}. Retrying in {delay}s...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=5, delay=2.0)
def load_flaky_data(path: str) -> pl.DataFrame:
# Simulate flaky I/O
return pl.read_csv(path)
Best practices make parameterized decorators safe, readable, and performant. Use factory pattern — outer function takes parameters, returns inner decorator. Always use @wraps(func) — preserves metadata. Modern tip: use Polars lazy API — decorate .collect() or .sink_* calls for retry/timing. Add type hints — def decorator(func: Callable) -> Callable — improves clarity and mypy checks. Handle *args, **kwargs — makes decorator generic. Return the wrapped result — chainable decorators. Use contextlib.ContextDecorator — class-based decorators that work as context managers. Test decorators independently — wrap dummy functions and assert behavior. Combine with tenacity — production-ready retry/backoff decorators. Use logging inside decorators — centralize logging without cluttering functions. Avoid decorators on generators — can break iteration; use wrappers carefully. Stack decorators thoughtfully — order matters (e.g., @retry @timer). Use functools.partial — pre-bind parameters for reusable decorator instances.
Decorators that take arguments use a factory pattern — outer function accepts parameters and returns the decorator. In 2026, use factory pattern, @wraps, type hints, generic args/kwargs, Polars profiling, and test thoroughly. Master parameterized decorators, and you’ll write flexible, configurable, reusable decorators that adapt behavior declaratively and elegantly.
Next time you need configurable decorator behavior — use a factory. It’s Python’s cleanest way to say: “Customize this decorator with parameters — @repeat(3).”