A decorator factory in Python is a higher-order function that takes parameters and returns a decorator — enabling customizable, parameterized decorators like @timer(message="Exec time"), @retry(attempts=3), or @repeat(5). The factory accepts arguments at decoration time, then returns the actual decorator that wraps the target function. This pattern separates configuration from wrapping logic, making decorators flexible, reusable, and declarative. In 2026, decorator factories are everywhere — powering configurable logging, timing with custom messages, retry/backoff policies, caching with maxsize, validation rules, rate limiting, and more in FastAPI, Flask, Celery, pandas/Polars pipelines, ML training loops, and production systems. They turn static decorators into dynamic tools that adapt to context without code duplication.
Here’s a complete, practical guide to decorator factories in Python: factory pattern, syntax and mechanics, preserving metadata with @wraps, real-world examples (timer with message, retry, repeat), and modern best practices with type hints, functools, logging, and pandas/Polars integration.
Basic decorator factory — outer function takes parameters, returns inner decorator that wraps the function.
def timer_factory(message: str = "Execution time"):
"""Factory: returns a timer decorator with custom message."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{message}: {end - start:.6f} seconds")
return result
return wrapper
return decorator
@timer_factory("Processing time")
def slow_sum(n: int) -> int:
return sum(range(n))
slow_sum(1_000_000)
# Processing time: 0.045678 seconds
Parameterized retry factory — configurable attempts and delay for flaky operations.
import time
from functools import wraps
def retry_factory(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_factory(max_attempts=5, delay=2.0)
def flaky_read(path: str) -> str:
# Simulate flaky I/O
if random.random() < 0.7:
raise IOError("Failed to read")
return "Success!"
flaky_read("data.txt")
Real-world pattern: configurable transformers in pandas/Polars — factory creates decorators with custom parameters for normalization, clipping, etc.
def clip_factory(lower: float = 0.0, upper: float = 1.0):
def decorator(func):
@wraps(func)
def wrapper(df: pd.DataFrame, *args, **kwargs):
result = func(df, *args, **kwargs)
return result.clip(lower=lower, upper=upper)
return wrapper
return decorator
@clip_factory(lower=0.05, upper=0.95)
def normalize(df: pd.DataFrame) -> pd.DataFrame:
return (df - df.mean()) / df.std()
df = pd.DataFrame({'value': [-10, 0, 50, 110]})
normalized = normalize(df)
print(normalized) # values clipped between 0.05 and 0.95
Best practices make decorator factories safe, readable, and performant. Use factory pattern — outer accepts parameters, returns inner decorator. Always use @wraps(func) — preserves metadata. Modern tip: use Polars lazy API — factories for configurable expressions or lazy frames. Add type hints — def factory(param: int) -> Callable[[Callable], Callable] — improves clarity and mypy checks. Handle *args, **kwargs — makes decorator generic. Return wrapped result — chainable decorators. Use contextlib.ContextDecorator — class-based decorators that work as context managers. Test factories — create decorator, apply to dummy function, assert behavior. Combine with tenacity — production-ready retry/backoff factories. Use logging inside decorators — centralize logging without cluttering functions. Avoid decorators on generators — can break iteration; use wrappers carefully. Stack factories thoughtfully — order matters. Use functools.partial — pre-bind parameters for reusable decorator instances.
Decorator factories take arguments and return customized decorators — enabling configurable behavior like @timer("Exec") or @retry(5). In 2026, use factory pattern, @wraps, type hints, Polars profiling, and test thoroughly. Master decorator factories, and you’ll write flexible, reusable, declarative decorators that adapt to any need elegantly.
Next time you need a customizable decorator — use a factory. It’s Python’s cleanest way to say: “Give me a decorator tailored to these parameters.”