10 Advanced Python Decorators to Replace Repetitive if‑else Logic and Clean Up Your Code
This article introduces ten practical Python decorator patterns—covering caching, timing, retry, rate‑limiting, logging, dependency injection, class‑wide decoration, singleton, role‑based access control, and context management—each explained with concrete code examples, output snapshots, and guidance on when and how to apply them.
Ever found yourself copying the same logging, caching, or retry logic across dozens of functions? This guide shows how Python decorators—often described as a "Swiss army knife" for code—can turn spaghetti code into clean, modular building blocks.
Why Decorators Are Powerful
Think of adding the same smart lighting system to every room in a house. The naive way is to install wiring manually in each room; the smart way is to integrate the system during construction. Decorators are the "smart way" for functions, separating cross‑cutting concerns (logging, caching, permissions) from core business logic.
Part One: Performance‑Optimization Decorators
1. TTL Cache Decorator (Automatic Expiration)
Use case: You use functools.lru_cache but need cached data to expire after a certain time.
Problem: Standard LRU cache never expires, potentially returning stale data.
Solution: A decorator that stores results with a timestamp and discards them after ttl_seconds have passed.
import functools
import time
def ttl_cache(ttl_seconds):
"""Cache decorator with automatic expiration"""
cache = {}
def decorator(func):
@functools.wraps(func)
def wrapper(*args):
now = time.time()
# Check if cache entry exists and is not expired
if args in cache:
value, timestamp = cache[args]
if now - timestamp < ttl_seconds:
return value
# Cache miss or expired – recompute
result = func(*args)
cache[args] = (result, now)
return result
return wrapper
return decorator
@ttl_cache(ttl_seconds=3)
def get_weather(city):
"""Simulate an expensive operation to fetch weather data"""
print(f"Fetching weather for {city}...")
time.sleep(0.5) # Simulate network latency
return f"Weather in {city}: Sunny, 25°C"
print("First call (compute and cache):")
print(get_weather("Beijing"))
print("
Second call (1 s later, from cache):")
time.sleep(1)
print(get_weather("Beijing"))
print("
Third call (4 s later, cache expired, recompute):")
time.sleep(4)
print(get_weather("Beijing"))Output:
First call (compute and cache):
Fetching weather for Beijing...
Weather in Beijing: Sunny, 25°C
Second call (1 s later, from cache):
Weather in Beijing: Sunny, 25°C
Third call (4 s later, cache expired, recompute):
Fetching weather for Beijing...
Weather in Beijing: Sunny, 25°C functools.wrapspreserves the original function's metadata.
The cache stores (result, timestamp) tuples keyed by args. Keyword arguments would need a more sophisticated key.
2. Execution‑Time Tracker (Performance Bottleneck Locator)
Use case: Your application slows down but you don't know which function is the culprit.
Problem: Manually inserting timing code is tedious and easy to forget.
Solution: A generic decorator that measures execution time with a high‑precision timer and prints the duration in milliseconds.
import time
import functools
def timeit(func):
"""Decorator that measures function execution time"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter() # High‑precision timer
result = func(*args, **kwargs)
elapsed = (time.perf_counter() - start) * 1000 # Convert to ms
# In a real project, use a logging system instead of print
print(f"[PERF] {func.__name__} executed in {elapsed:.2f} ms")
return result
return wrapper
@timeit
def process_large_data(n):
"""Simulate processing a large amount of data"""
return sum(i * i for i in range(n))
@timeit
def fetch_from_database(query):
"""Simulate a database query"""
time.sleep(0.1)
return f"Results for {query}"
process_large_data(1_000_000)
fetch_from_database("SELECT * FROM users")Output:
[PERF] process_large_data executed in 46.32 ms
[PERF] fetch_from_database executed in 100.15 msAdvanced tip: Combine the decorator with log levels and enable it only when performance analysis is needed.
Part Two: Robustness Decorators
3. Retry Decorator (Handle Unstable Operations)
Use case: Network requests, file I/O, or third‑party APIs that occasionally fail.
Problem: Repeating retry logic clutters code.
Solution: A configurable retry decorator with exponential back‑off.
import functools
import time
import random
def retry(retries=3, delay=1.0, backoff=2.0, exceptions=(Exception,)):
"""Retry decorator.
Parameters:
retries: maximum number of attempts
delay: initial delay in seconds
backoff: multiplier for exponential back‑off
exceptions: exception types that trigger a retry
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
for attempt in range(1, retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == retries:
print(f"All {retries} attempts failed. Last error: {e}")
raise
print(f"Attempt {attempt} failed: {e}. Retrying in {current_delay:.1f}s...")
time.sleep(current_delay)
current_delay *= backoff # Exponential back‑off
return None
return wrapper
return decorator
@retry(retries=4, delay=0.5, backoff=1.5, exceptions=(ConnectionError, TimeoutError))
def call_unstable_api(endpoint):
"""Simulate an unstable API call"""
if random.random() < 0.7: # 70% chance of failure
raise ConnectionError(f"Failed to connect to {endpoint}")
return f"Success from {endpoint}"
result = call_unstable_api("/api/data")
print(f"Final result: {result}")Sample output:
Attempt 1 failed: Failed to connect to /api/data. Retrying in 0.5s...
Attempt 2 failed: Failed to connect to /api/data. Retrying in 0.8s...
Attempt 3 failed: Failed to connect to /api/data. Retrying in 1.2s...
Final result: Success from /api/dataKey point: Exponential back‑off is the gold standard for handling transient network issues.
4. Rate‑Limit Decorator (Prevent API Abuse)
Use case: Public APIs, user‑action limits, or anti‑scraping measures.
Problem: Embedding rate‑limit checks inside business logic makes the code noisy.
Solution: A time‑based rate‑limit decorator.
import time
import functools
def rate_limit(calls_per_minute):
"""Decorator that limits the number of calls per minute"""
interval = 60.0 / calls_per_minute
def decorator(func):
last_called = [0.0] # Mutable closure state
@functools.wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.time() - last_called[0]
if elapsed < interval:
wait_time = interval - elapsed
print(f"Rate limit exceeded. Please wait {wait_time:.1f} seconds.")
return None
last_called[0] = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(calls_per_minute=10)
def send_notification(user_id, message):
"""Simulate sending a notification"""
print(f"Notification sent to user {user_id}: {message}")
return True
for i in range(15):
result = send_notification(123, f"Message {i}")
if result is None:
time.sleep(0.1)Thread‑safe version: Use a threading.Lock and a shared timestamp when the decorator is used in multi‑threaded environments.
Part Three: Maintainability Decorators
5. Simple Logging Decorator (Replace Debug Prints)
Use case: Temporary print statements left in production code clutter logs.
Problem: Logging logic mixes with business logic.
Solution: A non‑intrusive logging decorator that prints a timestamp, log level, function name, arguments, and return value.
import functools
import datetime
def log_call(level="INFO"):
"""Decorator that records function calls"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
arg_str = ", ".join([repr(arg) for arg in args])
kwarg_str = ", ".join([f"{k}={repr(v)}" for k, v in kwargs.items()])
all_args = ", ".join(filter(None, [arg_str, kwarg_str]))
print(f"[{timestamp}] [{level}] Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"[{timestamp}] [{level}] {func.__name__} returned {repr(result)}")
return result
return wrapper
return decorator
@log_call(level="DEBUG")
def calculate_discount(price, discount_rate=0.1, tax_rate=0.08):
"""Calculate price after discount and tax"""
discounted = price * (1 - discount_rate)
final_price = discounted * (1 + tax_rate)
return round(final_price, 2)
price = calculate_discount(100, discount_rate=0.2)
print(f"Final price: ${price}")Production advice: Replace print with the standard logging module and use log‑level filtering.
6. Dependency‑Injection Decorator (Avoid Global Variables)
Use case: Functions need access to shared resources such as a database connection, configuration, or logger.
Problem: Global variables or passing dependencies through many layers makes code hard to test.
Solution: A decorator that temporarily injects dependencies into the function's global namespace.
import functools
def inject(**dependencies):
"""Dependency‑injection decorator"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
original_globals = func.__globals__.copy()
try:
func.__globals__.update(dependencies)
return func(*args, **kwargs)
finally:
# Restore original globals
for key in dependencies:
func.__globals__.pop(key, None)
for key, value in original_globals.items():
if key not in func.__globals__:
func.__globals__[key] = value
return wrapper
return decorator
class Database:
def query(self, sql):
return f"Result: {sql}"
class Logger:
def info(self, msg):
print(f"[INFO] {msg}")
db = Database()
logger = Logger()
@inject(db=db, logger=logger)
def process_order(order_id):
logger.info(f"Processing order {order_id}")
result = db.query(f"SELECT * FROM orders WHERE id={order_id}")
logger.info(f"Query result: {result}")
return result
process_order(12345)Safer alternative: Pass dependencies explicitly as function arguments (see inject_safe in the original source).
7. Class‑Level Decorator (Decorate All Methods at Once)
Use case: You need to add the same behavior (logging, timing, permission checks) to every method of a class.
Problem: Decorating each method individually is repetitive.
Solution: A class decorator that iterates over the class’s attributes and wraps callable methods.
import types
import functools
import time
def time_all_methods(cls):
"""Decorator that times every method of a class"""
for attr_name in dir(cls):
if attr_name.startswith("_"):
continue
attr = getattr(cls, attr_name)
if callable(attr):
@functools.wraps(attr)
def timed_method(self, *args, __original_method=attr, **kwargs):
start = time.perf_counter()
result = __original_method(self, *args, **kwargs)
elapsed = (time.perf_counter() - start) * 1000
print(f"{cls.__name__}.{__original_method.__name__} took {elapsed:.2f} ms")
return result
setattr(cls, attr_name, timed_method)
return cls
@time_all_methods
class DataProcessor:
def __init__(self, data):
self.data = data
def filter_data(self):
"""Filter even numbers"""
time.sleep(0.05)
return [x for x in self.data if x % 2 == 0]
def transform_data(self):
"""Double each element"""
time.sleep(0.03)
return [x * 2 for x in self.data]
def process(self):
"""Full processing pipeline"""
filtered = self.filter_data()
transformed = self.transform_data()
return transformed
processor = DataProcessor(list(range(1000)))
result = processor.process()
print(f"Processed result length: {len(result)}")Note: This decorator affects all instances of the class. For per‑method decoration, consider metaclasses or explicit decorators.
Part Four: Advanced Architecture Decorators
8. Singleton Decorator (Ensure a Global Unique Instance)
Use case: Configuration managers, database connection pools, or cache managers that must exist only once.
Problem: Manually writing singleton boilerplate repeats across classes.
Solution: A generic singleton decorator.
import functools
def singleton(cls):
"""Singleton decorator"""
instances = {}
@functools.wraps(cls)
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class AppConfig:
def __init__(self):
print("Initializing AppConfig...")
self.settings = {
"debug": True,
"database_url": "postgresql://localhost/mydb",
"cache_ttl": 300,
}
def get(self, key):
return self.settings.get(key)
def set(self, key, value):
self.settings[key] = value
config1 = AppConfig()
config2 = AppConfig()
print(f"config1 is config2: {config1 is config2}")
config1.set("debug", False)
print(f"config2.get('debug'): {config2.get('debug')}")Thread‑safe version: Use a double‑checked lock with threading.Lock to guard instance creation.
9. Role‑Based Access Control (RBAC) Decorator
Use case: Web applications, APIs, or admin back‑ends that need permission checks.
Problem: Repeating role verification in every protected function.
Solution: A decorator that checks the caller’s role against an allowed list and raises PermissionError if unauthorized.
import functools
def require_role(*allowed_roles):
"""Decorator for role‑based permission checks"""
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
user_role = user.get("role", "guest")
if user_role not in allowed_roles:
raise PermissionError(
f"User {user.get('name', 'Unknown')} with role '{user_role}' "
f"is not allowed to perform {func.__name__}. "
f"Required roles: {allowed_roles}"
)
return func(user, *args, **kwargs)
return wrapper
return decorator
admin_user = {"id": 1, "name": "Alice", "role": "admin"}
editor_user = {"id": 2, "name": "Bob", "role": "editor"}
viewer_user = {"id": 3, "name": "Charlie", "role": "viewer"}
class ContentManagementSystem:
@require_role("admin", "editor")
def create_article(self, user, title, content):
print(f"{user['name']} created article: {title}")
return {"id": 100, "title": title, "content": content}
@require_role("admin", "editor", "viewer")
def view_article(self, user, article_id):
print(f"{user['name']} viewed article {article_id}")
return {"id": article_id, "content": "Sample content"}
@require_role("admin")
def delete_article(self, user, article_id):
print(f"{user['name']} deleted article {article_id}")
return {"success": True}
cms = ContentManagementSystem()
cms.create_article(admin_user, "Python Tips", "Some content")
cms.view_article(viewer_user, 100)
try:
cms.delete_article(editor_user, 100)
except PermissionError as e:
print(f"Permission error: {e}")Output:
Alice created article: Python Tips
Charlie viewed article 100
Permission error: User Bob with role 'editor' is not allowed to perform delete_article. Required roles: ('admin',)10. Context‑Management Decorator (Pass Request‑Level Information)
Use case: Micro‑services, asynchronous tasks, or web request handling where a request ID, user info, or other context needs to flow through many functions.
Problem: Propagating context via function arguments becomes cumbersome.
Solution: A decorator built on contextvars that automatically injects and logs context information.
import functools
import contextvars
import uuid
import time
# Define context variables
request_id_var = contextvars.ContextVar("request_id", default=None)
user_context_var = contextvars.ContextVar("user", default=None)
def with_context(func):
"""Decorator that manages request context"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
request_id = request_id_var.get()
user = user_context_var.get()
if request_id is None:
request_id = str(uuid.uuid4())[:8]
request_id_var.set(request_id)
print(f"[{request_id}] Starting {func.__name__}")
if user:
print(f"[{request_id}] User: {user.get('name', 'Unknown')}")
try:
result = func(*args, **kwargs)
print(f"[{request_id}] {func.__name__} completed successfully")
return result
except Exception as e:
print(f"[{request_id}] {func.__name__} failed: {e}")
raise
return wrapper
def set_request_context(request_id=None, user=None):
"""Helper to set request‑level context"""
if request_id:
request_id_var.set(request_id)
if user:
user_context_var.set(user)
@with_context
def authenticate(token):
print(f"Authenticating token: {token[:10]}...")
return {"id": 123, "name": "John Doe", "role": "user"}
@with_context
def process_request(data):
user = user_context_var.get()
print(f"Processing request for user: {user['name']}")
return {"status": "success", "data": data}
@with_context
def handle_api_request(token, request_data):
user = authenticate(token)
user_context_var.set(user)
result = process_request(request_data)
request_id = request_id_var.get()
print(f"[{request_id}] API request completed")
return result
print("=== Test 1: Normal request ===")
set_request_context(request_id="req_001")
response = handle_api_request("secure_token_123", {"action": "get_data"})
print(f"Response: {response}")
print("
=== Test 2: Another request ===")
set_request_context(request_id="req_002", user={"id": 456, "name": "Jane Smith"})
response = handle_api_request("secure_token_456", {"action": "update_data"})
print(f"Response: {response}")Sample output:
=== Test 1: Normal request ===
[req_001] Starting handle_api_request
Authenticating token: secure_tok...
[req_001] Starting authenticate
[req_001] authenticate completed successfully
[req_001] Starting process_request
Processing request for user: John Doe
[req_001] process_request completed successfully
[req_001] API request completed
Response: {'status': 'success', 'data': {'action': 'get_data'}}
=== Test 2: Another request ===
[req_002] Starting handle_api_request
[req_002] User: Jane Smith
Authenticating token: secure_tok...
[req_002] Starting authenticate
[req_002] authenticate completed successfully
[req_002] Starting process_request
Processing request for user: John Doe
[req_002] process_request completed successfully
[req_002] API request completed
Response: {'status': 'success', 'data': {'action': 'update_data'}}Note: contextvars is available from Python 3.7 and propagates automatically across asynchronous tasks, unlike threading.local.
Conclusion
We have explored ten advanced Python decorator patterns that address real‑world pain points: performance, robustness, maintainability, and architectural concerns. The core value of decorators is the clean separation of cross‑cutting concerns from business logic, yielding code that is easier to read, maintain, and reuse.
Cleaner code – business logic stays free of auxiliary boilerplate.
Easier maintenance – changes to a concern are made in a single place.
Higher reusability – decorators can be shared across projects.
Improved readability – a decorator’s name instantly conveys the added behavior.
Final tip: Keep decorators simple and transparent, always use functools.wraps to preserve metadata, and write unit tests for complex decorators. Overusing decorators can make debugging harder, so apply them judiciously.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Data STUDIO
Click to receive the "Python Study Handbook"; reply "benefit" in the chat to get it. Data STUDIO focuses on original data science articles, centered on Python, covering machine learning, data analysis, visualization, MySQL and other practical knowledge and project case studies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
