Advanced Lesson
Building & Reusing Custom Decorator Libraries
Design reusable decorator libraries that power frameworks, APIs, and production systems
Why Build Your Own Decorator Library?
High-level Python systems need common behaviors that decorators provide:
- Logging — Track function calls and results
- Caching — Store expensive computation results
- Error handling & retries — Recover from failures
- Validation — Enforce input/output constraints
- Timing — Measure performance
- Access control — Enforce permissions
- Registration — Build plugin systems
A centralized decorator library provides tested utilities, ensures consistent behavior, and speeds up development.
The Core Pattern
Every reusable decorator follows this pattern. Always use functools.wraps to preserve metadata.
Decorator Template
import functools
def decorator_template(func):
"""Template for creating decorators."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Before: code that runs before the function
print(f"Calling {func.__name__}")
# Execute the original function
result = func(*args, **kwargs)
# After: code that runs after the function
print(f"Finished {func.__name__}")
return result
return wrapper
...Reusable Timer Decorator
A practical decorator for measuring function execution time.
Timing Decorator
import functools
import time
def timing(func):
"""Measure and print function execution time."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
duration = time.perf_counter() - start
print(f"{func.__name__} took {duration:.4f}s")
return result
return wrapper
# Example usage
@timing
def calculate_sum(n):
"""Calculate sum of numbers from 1 to n."""
return sum(
...Decorators With Arguments (Decorator Factories)
To accept arguments, wrap your decorator in another function that returns the actual decorator.
Retry Decorator Factory
import functools
import time
def retry(times=3, delay=0):
"""
Retry a function if it fails.
Args:
times: Number of retry attempts
delay: Seconds to wait between retries
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Excepti
...Reusable Logging Decorator
Log function calls with customizable logger for debugging and monitoring.
Logging Decorator
import functools
def log_calls(logger=print):
"""
Log function calls with arguments and return values.
Args:
logger: Callable for logging (default: print)
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Format arguments
args_str = ", ".join(repr(a) for a in args)
kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
all_args = ", ".join(filter(None, [args_st
...Type Validation Decorator
Enforce type checking using function annotations for safer code.
Type Validation Decorator
import functools
import inspect
def enforce_types(func):
"""Validate argument types based on annotations."""
sig = inspect.signature(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Bind arguments to parameters
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
# Check each parameter
for param_name, param_value in bound.arguments.items():
param = sig.parameters[param_name]
...Access Control Decorator
Enforce role-based access control, commonly used in web frameworks and APIs.
Access Control Decorator
import functools
def require_role(required_role):
"""
Require a specific role to execute function.
Args:
required_role: Role required to access the function
"""
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not hasattr(user, 'role'):
raise AttributeError("User object must have 'role' attribute")
if user.role != required_role:
raise Permission
...Memoization / Caching Decorator
Cache expensive function results to improve performance.
Memoization Decorator
import functools
def memoize(func):
"""Cache function results based on arguments."""
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
print(f"Computing {func.__name__}{args}...")
cache[args] = func(*args)
else:
print(f"Using cached result for {func.__name__}{args}")
return cache[args]
# Expose cache for inspection
wrapper.cache = cache
return wrapper
# Example:
...Registry Pattern for Plugins
Automatically register functions or classes for plugin systems and routing.
Registry Pattern
import functools
# Global registry
command_registry = {}
def register_command(name):
"""
Register a function as a command handler.
Args:
name: Command name for the registry
"""
def decorator(func):
command_registry[name] = func
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
# Register handlers
@register_command("greet")
def handl
...Async-Compatible Decorators
Support both sync and async functions in your decorator library.
Universal Timer
import functools
import inspect
import time
def universal_timer(func):
"""Timer that works with both sync and async functions."""
if inspect.iscoroutinefunction(func):
# Async version
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
start = time.perf_counter()
result = await func(*args, **kwargs)
duration = time.perf_counter() - start
print(f"{func.__name__} took {duration:.4f}s (async)")
...Stacking Decorators Properly
Multiple decorators execute bottom-to-top, but wrap top-to-bottom. Order matters!
Decorator Stacking
import functools
# Three simple decorators
def decorator_a(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("A: Before")
result = func(*args, **kwargs)
print("A: After")
return result
return wrapper
def decorator_b(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("B: Before")
result = func(*args, **kwargs)
print("B: After")
return result
return wrapper
def decorator_c(func
...Building a Complete Decorator Library
Organize decorators into a reusable library structure for production use.
Decorator Library Structure
"""
Example decorator library structure:
decorlib/
__init__.py # Public API exports
timing.py # Performance decorators
logging.py # Logging decorators
validation.py # Input validation
caching.py # Memoization
retry.py # Error handling
auth.py # Access control
registry.py # Plugin registration
async_tools.py # Async utilities
Usage:
from decorlib import timing, log_
...Advanced: Exponential Backoff Retry
Production-grade retry decorator with exponential backoff and jitter.
Exponential Backoff Retry
import functools
import time
import random
def backoff_retry(max_attempts=3, base_delay=0.1, max_delay=10, jitter=True):
"""
Retry with exponential backoff.
Args:
max_attempts: Maximum number of retry attempts
base_delay: Initial delay in seconds
max_delay: Maximum delay between retries
jitter: Add random jitter to prevent thundering herd
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
...Summary
You've learned to build production-grade decorator libraries:
- Core decorator pattern with functools.wraps
- Decorator factories for parameterized decorators
- Timing and performance measurement
- Logging with customizable loggers
- Type validation using annotations
- Access control and permissions
- Memoization and caching
- Registry patterns for plugins
- Async-compatible decorators
- Proper decorator stacking
- Exponential backoff retry
- Library organization and best practices
Decorator libraries power major frameworks like Django, FastAPI, Flask, and Celery. A well-designed decorator library becomes reusable infrastructure that speeds up development across multiple projects.
📋 Quick Reference — Decorator Libraries
| Pattern | Use case |
|---|---|
| @functools.wraps(fn) | Preserve original __name__ and __doc__ |
| def decorator(fn): ... | Basic decorator factory |
| def with_args(n): ... | Decorator that accepts arguments |
| class-based decorator | Decorator with persistent state |
| @retry(max=3, delay=1) | Parameterised production decorator |
🎉 Great work! You've completed this lesson.
You can now build reusable decorator libraries — retry logic, rate limiting, caching, auth guards — used across entire codebases.
Up next: SQLite & ORMs — work with databases using both raw SQL and SQLAlchemy.
Sign up for free to track which lessons you've completed and get learning reminders.