Skip to content

Decorators

Enhance functions and classes with decorators - Python's elegant way to add functionality.

Decorators in Python are a powerful feature that allows you to modify or extend the behavior of functions and classes without permanently modifying them. They provide a clean, readable way to add functionality like logging, caching, authentication, and more.

Without decorators (repetitive code):

without_decorators.py
def greet(name: str) -> str:
print("Function called: greet")
print(f"Arguments: name={name}")
start_time = time.time()
result = f"Hello, {name}!"
end_time = time.time()
print(f"Execution time: {end_time - start_time:.4f} seconds")
return result
def calculate_sum(a: int, b: int) -> int:
print("Function called: calculate_sum")
print(f"Arguments: a={a}, b={b}")
start_time = time.time()
result = a + b
end_time = time.time()
print(f"Execution time: {end_time - start_time:.4f} seconds")
return result
# Problem: Same logging code repeated in every function!

With decorators (clean and reusable):

with_decorators.py
import time
from functools import wraps
def log_execution(func):
"""Decorator to log function execution"""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Function called: {func.__name__}")
print(f"Arguments: {args}, {kwargs}")
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Execution time: {end_time - start_time:.4f} seconds")
return result
return wrapper
@log_execution
def greet(name: str) -> str:
return f"Hello, {name}!"
@log_execution
def calculate_sum(a: int, b: int) -> int:
return a + b
# Clean, reusable, and maintainable!

A decorator is essentially a function that:

  1. Takes a function as input
  2. Returns a new function (usually a wrapper)
  3. The wrapper calls the original function with added behavior
basic_decorator.py
def my_decorator(func):
"""Basic decorator structure"""
def wrapper(*args, **kwargs):
# Code to execute BEFORE the function
print("Before function execution")
# Call the original function
result = func(*args, **kwargs)
# Code to execute AFTER the function
print("After function execution")
return result
return wrapper
@my_decorator
def say_hello(name: str):
print(f"Hello, {name}!")
# Usage
say_hello("Alice")
# Output:
# Before function execution
# Hello, Alice!
# After function execution

When creating decorators, it’s important to preserve the original function’s metadata (name, docstring, etc.):

preserving_metadata.py
from functools import wraps
def my_decorator(func):
"""Decorator that preserves function metadata"""
@wraps(func) # Preserves function metadata
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name: str) -> str:
"""Greet someone"""
return f"Hello, {name}!"
# Without @wraps:
print(greet.__name__) # Would print "wrapper"
print(greet.__doc__) # Would print None
# With @wraps:
print(greet.__name__) # Prints "greet"
print(greet.__doc__) # Prints "Greet someone"

Measure how long a function takes to execute:

timing_decorator.py
import time
from functools import wraps
def timing(func):
"""Decorator to measure function execution time"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper
@timing
def slow_function():
"""Simulate a slow operation"""
time.sleep(1)
return "Done"
@timing
def fast_function():
"""A fast operation"""
return "Quick!"
# Usage
slow_function() # Prints: slow_function took 1.0001 seconds
fast_function() # Prints: fast_function took 0.0001 seconds

Log function calls and arguments:

logging_decorator.py
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def log_calls(func):
"""Decorator to log function calls"""
@wraps(func)
def wrapper(*args, **kwargs):
logger.info(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
try:
result = func(*args, **kwargs)
logger.info(f"{func.__name__} returned: {result}")
return result
except Exception as e:
logger.error(f"{func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
@log_calls
def divide(a: float, b: float) -> float:
"""Divide two numbers"""
return a / b
# Usage
divide(10, 2) # Logs: Calling divide with args=(10, 2), kwargs={}
divide(10, 0) # Logs error and raises ZeroDivisionError

Cache function results to avoid recomputation:

caching_decorator.py
from functools import wraps
def simple_cache(func):
"""Simple caching decorator"""
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
# Create cache key from arguments
key = str(args) + str(sorted(kwargs.items()))
if key in cache:
print(f"Cache hit for {func.__name__}")
return cache[key]
print(f"Cache miss for {func.__name__}, computing...")
result = func(*args, **kwargs)
cache[key] = result
return result
return wrapper
@simple_cache
def expensive_computation(n: int) -> int:
"""Expensive computation that we want to cache"""
print(f"Computing for n={n}...")
return n * n
# Usage
expensive_computation(5) # Cache miss, computes
expensive_computation(5) # Cache hit, returns cached result
expensive_computation(3) # Cache miss, computes
expensive_computation(3) # Cache hit, returns cached result

Automatically retry a function if it fails:

retry_decorator.py
import time
from functools import wraps
def retry(max_attempts: int = 3, delay: float = 1.0):
"""Decorator to retry a function on failure"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise # Last attempt, raise the exception
print(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay}s...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=1.0)
def unreliable_function():
"""Function that might fail"""
import random
if random.random() < 0.7: # 70% chance of failure
raise ValueError("Random failure!")
return "Success!"
# Usage - will retry up to 3 times
unreliable_function()

Validate function arguments:

validation_decorator.py
from functools import wraps
def validate_positive(func):
"""Decorator to validate that arguments are positive"""
@wraps(func)
def wrapper(*args, **kwargs):
# Check all positional arguments
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(f"Argument {arg} must be positive")
# Check all keyword arguments
for key, value in kwargs.items():
if isinstance(value, (int, float)) and value < 0:
raise ValueError(f"Argument {key}={value} must be positive")
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(length: float, width: float) -> float:
"""Calculate area of a rectangle"""
return length * width
# Usage
calculate_area(5, 4) # Works fine
calculate_area(-5, 4) # Raises ValueError

Sometimes you need decorators that accept arguments. This requires an extra layer:

decorator_with_args.py
from functools import wraps
def repeat(times: int):
"""Decorator that repeats function execution"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
result = func(*args, **kwargs)
results.append(result)
return results
return wrapper
return decorator
@repeat(times=3)
def greet(name: str) -> str:
"""Greet someone"""
return f"Hello, {name}!"
# Usage
result = greet("Alice")
print(result) # ['Hello, Alice!', 'Hello, Alice!', 'Hello, Alice!']

Decorators can also be applied to classes:

class_decorator.py
def add_methods(cls):
"""Class decorator that adds methods to a class"""
def new_method(self):
return f"New method in {self.__class__.__name__}"
cls.new_method = new_method
return cls
@add_methods
class MyClass:
def __init__(self, value: int):
self.value = value
# Usage
obj = MyClass(42)
print(obj.new_method()) # Prints: "New method in MyClass"
singleton_decorator.py
def singleton(cls):
"""Class decorator to make a class a singleton"""
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
def __init__(self):
print("Creating database connection...")
self.connection_id = id(self)
# Usage
db1 = DatabaseConnection() # Prints: Creating database connection...
db2 = DatabaseConnection() # No print, returns same instance
print(db1.connection_id == db2.connection_id) # True - same instance!

Python provides several useful built-in decorators:

Create properties with getters, setters, and deleters:

property_decorator.py
class Circle:
def __init__(self, radius: float):
self._radius = radius
@property
def radius(self) -> float:
"""Get the radius"""
return self._radius
@radius.setter
def radius(self, value: float):
"""Set the radius with validation"""
if value < 0:
raise ValueError("Radius must be positive")
self._radius = value
@radius.deleter
def radius(self):
"""Delete the radius"""
del self._radius
@property
def area(self) -> float:
"""Calculate area (read-only property)"""
return 3.14159 * self._radius ** 2
# Usage
circle = Circle(5.0)
print(circle.radius) # 5.0
print(circle.area) # 78.53975
circle.radius = 10.0 # Uses setter
print(circle.area) # 314.159
circle.radius = -5 # Raises ValueError
static_class_methods.py
class MathUtils:
PI = 3.14159
@staticmethod
def add(a: int, b: int) -> int:
"""Static method - doesn't need class or instance"""
return a + b
@classmethod
def get_pi(cls) -> float:
"""Class method - receives class as first argument"""
return cls.PI
def instance_method(self):
"""Instance method - receives instance as first argument"""
return f"Instance of {self.__class__.__name__}"
# Usage
print(MathUtils.add(5, 3)) # 8 - called on class
print(MathUtils.get_pi()) # 3.14159 - called on class
print(MathUtils().instance_method()) # Instance of MathUtils - needs instance

Cache function results with least-recently-used eviction:

lru_cache.py
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
"""Calculate Fibonacci number with caching"""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Usage
print(fibonacci(30)) # Fast due to caching
print(fibonacci.cache_info()) # Shows cache statistics

Preserve function metadata (we’ve seen this already):

wraps_example.py
from functools import wraps
def my_decorator(func):
@wraps(func) # Preserves name, docstring, etc.
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
rate_limiting.py
import time
from functools import wraps
from collections import defaultdict
def rate_limit(calls_per_second: float):
"""Decorator to rate limit function calls"""
min_interval = 1.0 / calls_per_second
last_called = defaultdict(float)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
elapsed = now - last_called[func]
if elapsed < min_interval:
time.sleep(min_interval - elapsed)
last_called[func] = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(calls_per_second=2.0) # Max 2 calls per second
def api_call():
"""Simulate an API call"""
return "API response"
# Usage - calls are automatically rate-limited
for _ in range(5):
print(api_call())
time.sleep(0.1) # Without decorator, would call 5 times quickly
authentication.py
from functools import wraps
# Simulated user database
users = {
"alice": {"password": "secret123", "role": "admin"},
"bob": {"password": "password", "role": "user"}
}
current_user = None
def login(username: str, password: str) -> bool:
"""Simulate login"""
global current_user
if username in users and users[username]["password"] == password:
current_user = username
return True
return False
def requires_auth(func):
"""Decorator to require authentication"""
@wraps(func)
def wrapper(*args, **kwargs):
if current_user is None:
raise PermissionError("Authentication required")
return func(*args, **kwargs)
return wrapper
def requires_role(role: str):
"""Decorator to require specific role"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if current_user is None:
raise PermissionError("Authentication required")
if users[current_user]["role"] != role:
raise PermissionError(f"Role '{role}' required")
return func(*args, **kwargs)
return wrapper
return decorator
@requires_auth
def get_profile():
"""Get user profile - requires authentication"""
return f"Profile for {current_user}"
@requires_role("admin")
def delete_user(username: str):
"""Delete user - requires admin role"""
return f"Deleted user {username}"
# Usage
login("alice", "secret123")
print(get_profile()) # Works
print(delete_user("bob")) # Works (alice is admin)
login("bob", "password")
print(get_profile()) # Works
print(delete_user("alice")) # Raises PermissionError (bob is not admin)
combined_decorators.py
import time
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def timing(func):
"""Timing decorator"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
duration = time.time() - start
logger.info(f"{func.__name__} took {duration:.4f}s")
return result
return wrapper
def log_calls(func):
"""Logging decorator"""
@wraps(func)
def wrapper(*args, **kwargs):
logger.info(f"Calling {func.__name__} with args={args}")
return func(*args, **kwargs)
return wrapper
@timing
@log_calls
def process_data(data: list) -> list:
"""Process data with timing and logging"""
time.sleep(0.1) # Simulate processing
return [x * 2 for x in data]
# Usage
result = process_data([1, 2, 3, 4, 5])
# Logs:
# Calling process_data with args=([1, 2, 3, 4, 5],)
# process_data took 0.1001s
forgetting_wraps.py
# ❌ Bad - loses function metadata
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# ✅ Good - preserves function metadata
from functools import wraps
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
exception_handling.py
# ❌ Bad - exceptions break the decorator
def bad_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs) # If this raises, "After" never prints
print("After")
return result
return wrapper
# ✅ Good - handles exceptions properly
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Before")
try:
result = func(*args, **kwargs)
print("After")
return result
except Exception as e:
print(f"Error: {e}")
raise
return wrapper

Mistake 3: Modifying Mutable Default Arguments

Section titled “Mistake 3: Modifying Mutable Default Arguments”
mutable_defaults.py
# ❌ Bad - shared mutable state
def bad_decorator(func):
cache = {} # Shared across all decorated functions!
@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key in cache:
return cache[key]
result = func(*args, **kwargs)
cache[key] = result
return result
return wrapper
# ✅ Good - separate cache per function
def good_decorator(func):
cache = {} # Each decorated function gets its own cache
@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key in cache:
return cache[key]
result = func(*args, **kwargs)
cache[key] = result
return result
wrapper.cache = cache # Attach cache to wrapper
return wrapper
  1. Always use @wraps(func) - Preserves function metadata
  2. Handle exceptions properly - Don’t let decorators hide errors
  3. Keep decorators simple - One decorator, one responsibility
  4. Use type hints - Makes decorators more maintainable
  5. Document decorators - Explain what they do and any side effects
  6. Test decorators - Ensure they work with various function signatures
  • Decorators are functions that wrap other functions or classes
  • They provide a clean way to add cross-cutting concerns
  • Use @wraps(func) to preserve function metadata
  • Decorators can accept arguments (decorator factories)
  • Decorators can be applied to both functions and classes
  • Python provides built-in decorators like @property, @staticmethod, @lru_cache
  • Multiple decorators can be stacked (execute bottom to top)
  • Always handle exceptions properly in decorators

Remember: Decorators let you enhance functions and classes without modifying their core logic - write once, enhance everywhere!