Python Decorators: Enhancing Functions with Metaprogramming

Decorators are a powerful metaprogramming feature in Python enabling modification of function or class behavior without altering their source code, using the @ syntax to wrap functions with additional functionality. Decorators leverage Python's first-class function support where functions can be passed as arguments, returned from other functions, and assigned to variables, creating wrapper functions that execute code before and after the original function while preserving its interface. This pattern enables cross-cutting concerns like logging, timing, authentication, caching, and validation to be applied declaratively, separating these aspects from business logic and promoting code reusability through composable behavior modifiers applied consistently across multiple functions or classes.
This comprehensive guide explores decorator fundamentals with functions as first-class objects enabling higher-order functions, basic function decorator syntax wrapping functions with additional behavior, the @ syntax providing elegant decorator application, decorators with arguments requiring decorator factories, functools.wraps preserving original function metadata preventing loss of docstrings and names, multiple decorators stacking to compose behavior, class decorators modifying class definitions, decorators for methods handling self parameter correctly, built-in decorators including @property, @staticmethod, and @classmethod, practical applications like timing functions, logging calls, caching results with memoization, authentication checks, and retry logic, and best practices preserving function signatures, using wraps for metadata, keeping decorators simple, and documenting decorator behavior. Whether you're implementing cross-cutting concerns, building frameworks requiring behavior modification hooks, creating APIs with authentication and rate limiting, optimizing performance with caching, or adding debugging instrumentation, mastering decorators provides essential tools for writing clean, maintainable Python code that separates concerns through composable behavior modification patterns.
Decorator Fundamentals
Decorators are functions that take another function as input and return a new function that wraps the original, adding behavior before or after execution. The @ syntax provides syntactic sugar for applying decorators, making code more readable than explicit function wrapping. Understanding that decorators are called at function definition time, not at call time, is crucial for proper decorator design and avoiding common pitfalls.
# Decorator Fundamentals
# === Functions as first-class objects ===
# Functions can be assigned to variables
def greet(name):
return f"Hello, {name}!"
say_hello = greet
print(say_hello("Alice")) # Hello, Alice!
# Functions can be passed as arguments
def execute_function(func, value):
return func(value)
result = execute_function(greet, "Bob")
print(result) # Hello, Bob!
# Functions can return other functions
def create_multiplier(factor):
def multiplier(x):
return x * factor
return multiplier
double = create_multiplier(2)
print(double(5)) # 10
# === Basic decorator ===
def my_decorator(func):
"""Basic decorator wrapper."""
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
# Manual decoration (without @ syntax)
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)
say_hello()
# Output:
# Before function call
# Hello!
# After function call
# === Using @ syntax ===
@my_decorator
def say_goodbye():
print("Goodbye!")
say_goodbye()
# Output:
# Before function call
# Goodbye!
# After function call
# The @ syntax is equivalent to:
# say_goodbye = my_decorator(say_goodbye)
# === Decorator with arguments ===
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Finished {func.__name__}")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
result = add(3, 5)
print(f"Result: {result}")
# Output:
# Calling add
# Finished add
# Result: 8
# === Decorator preserving return value ===
@my_decorator
def multiply(x, y):
"""Multiply two numbers."""
return x * y
result = multiply(4, 7)
print(result) # 28
# === Problem: Lost metadata ===
print(multiply.__name__) # wrapper (should be 'multiply')
print(multiply.__doc__) # None (should be docstring)
# === Solution: functools.wraps ===
from functools import wraps
def my_decorator(func):
@wraps(func) # Preserves original function metadata
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
return result
return wrapper
@my_decorator
def divide(a, b):
"""Divide two numbers."""
return a / b
print(divide.__name__) # divide (correct!)
print(divide.__doc__) # Divide two numbers. (correct!)
# === Multiple decorators (stacking) ===
def decorator_one(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Decorator 1")
return func(*args, **kwargs)
return wrapper
def decorator_two(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Decorator 2")
return func(*args, **kwargs)
return wrapper
@decorator_one
@decorator_two
def my_function():
print("Original function")
my_function()
# Output:
# Decorator 1
# Decorator 2
# Original function
# Applied bottom-up: my_function = decorator_one(decorator_two(my_function))functools.wraps, decorators lose the original function's name, docstring, and other metadata. Always decorate your wrapper with @wraps(func).Decorators with Arguments
Decorators that accept arguments require an additional level of nesting, creating decorator factories that return actual decorators. This pattern enables parameterized decorators customizing behavior based on arguments, like specifying repetition counts, logging levels, or timeout durations. The extra function layer handles decorator arguments while the inner layers handle the decorated function and its execution.
# Decorators with Arguments
from functools import wraps
import time
# === Decorator factory pattern ===
def repeat(times):
"""Decorator that repeats function execution."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!
# Equivalent to:
# greet = repeat(times=3)(greet)
# === Decorator with optional arguments ===
def smart_decorator(func=None, *, prefix=">>>"):
"""Decorator that works with or without arguments."""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
print(f"{prefix} Calling {f.__name__}")
return f(*args, **kwargs)
return wrapper
if func is None:
# Called with arguments: @smart_decorator(prefix="***")
return decorator
else:
# Called without arguments: @smart_decorator
return decorator(func)
# Without arguments
@smart_decorator
def func1():
print("Function 1")
func1()
# Output:
# >>> Calling func1
# Function 1
# With arguments
@smart_decorator(prefix="***")
def func2():
print("Function 2")
func2()
# Output:
# *** Calling func2
# Function 2
# === Timing decorator with threshold ===
def timer(threshold=1.0):
"""Log execution time if exceeds threshold."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
if elapsed > threshold:
print(f"{func.__name__} took {elapsed:.2f}s (threshold: {threshold}s)")
return result
return wrapper
return decorator
@timer(threshold=0.5)
def slow_function():
time.sleep(1)
return "Done"
result = slow_function()
# Output: slow_function took 1.00s (threshold: 0.5s)
# === Logging decorator with level ===
def log(level="INFO"):
"""Log function calls with specified level."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{level}] Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[{level}] {func.__name__} returned {result}")
return result
return wrapper
return decorator
@log(level="DEBUG")
def add(a, b):
return a + b
result = add(3, 5)
# Output:
# [DEBUG] Calling add with args=(3, 5), kwargs={}
# [DEBUG] add returned 8
# === Retry decorator ===
def retry(max_attempts=3, delay=1):
"""Retry function on exception."""
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
print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if random.random() < 0.7:
raise ValueError("Random failure")
return "Success"
# Will retry up to 3 times
# result = unreliable_function()
# === Validation decorator ===
def validate_types(**type_checks):
"""Validate argument types."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Check types
for arg_name, expected_type in type_checks.items():
if arg_name in kwargs:
value = kwargs[arg_name]
if not isinstance(value, expected_type):
raise TypeError(
f"{arg_name} must be {expected_type}, got {type(value)}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return f"User: {name}, Age: {age}"
result = create_user(name="Alice", age=25) # OK
print(result)
# This would raise TypeError:
# create_user(name="Alice", age="25")def decorator(arg): def actual_decorator(func): def wrapper...Class-Based Decorators
Class-based decorators use classes instead of functions to implement decorator behavior, defining __init__() to receive the decorated function and __call__() to execute wrapper logic. This approach enables stateful decorators maintaining call counts, caching results, or tracking execution history using instance variables. Class decorators also improve readability for complex decorator logic by organizing functionality into methods rather than nested functions.
# Class-Based Decorators
from functools import wraps
import time
# === Basic class decorator ===
class SimpleDecorator:
"""Basic class-based decorator."""
def __init__(self, func):
self.func = func
wraps(func)(self) # Preserve metadata
def __call__(self, *args, **kwargs):
print(f"Calling {self.func.__name__}")
result = self.func(*args, **kwargs)
print(f"Finished {self.func.__name__}")
return result
@SimpleDecorator
def greet(name):
return f"Hello, {name}!"
print(greet("Alice"))
# Output:
# Calling greet
# Finished greet
# Hello, Alice!
# === Stateful decorator: Call counter ===
class CountCalls:
"""Count how many times function is called."""
def __init__(self, func):
self.func = func
self.count = 0
wraps(func)(self)
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} called {self.count} times")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # say_hello called 1 times
say_hello() # say_hello called 2 times
say_hello() # say_hello called 3 times
print(f"Total calls: {say_hello.count}")
# === Class decorator with arguments ===
class Timer:
"""Time function execution with optional threshold."""
def __init__(self, threshold=0.0):
self.threshold = threshold
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
if elapsed > self.threshold:
print(f"{func.__name__} took {elapsed:.3f}s")
return result
return wrapper
@Timer(threshold=0.1)
def slow_function():
time.sleep(0.2)
return "Done"
result = slow_function()
# Output: slow_function took 0.200s
# === Memoization decorator ===
class Memoize:
"""Cache function results."""
def __init__(self, func):
self.func = func
self.cache = {}
wraps(func)(self)
def __call__(self, *args):
if args not in self.cache:
print(f"Computing {self.func.__name__}{args}")
self.cache[args] = self.func(*args)
else:
print(f"Using cached result for {args}")
return self.cache[args]
@Memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(5))
# Output:
# Computing fibonacci(5)
# Computing fibonacci(4)
# ...
# (subsequent calls use cache)
# === Rate limiting decorator ===
class RateLimit:
"""Limit function call rate."""
def __init__(self, max_calls, period):
self.max_calls = max_calls
self.period = period
self.calls = []
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# Remove old calls
self.calls = [call for call in self.calls if now - call < self.period]
if len(self.calls) >= self.max_calls:
raise Exception(f"Rate limit exceeded: {self.max_calls} calls per {self.period}s")
self.calls.append(now)
return func(*args, **kwargs)
return wrapper
@RateLimit(max_calls=3, period=1.0)
def api_call():
print("API called")
# First 3 calls succeed
api_call()
api_call()
api_call()
# 4th call would raise exception:
# api_call() # Exception: Rate limit exceeded
# === Decorating classes ===
def add_str_method(cls):
"""Add __str__ method to class."""
def __str__(self):
return f"{cls.__name__} instance"
cls.__str__ = __str__
return cls
@add_str_method
class MyClass:
pass
obj = MyClass()
print(obj) # MyClass instance
# === Class decorator adding attributes ===
def add_timestamp(cls):
"""Add creation timestamp to class."""
import datetime
cls.created_at = datetime.datetime.now()
return cls
@add_timestamp
class Document:
pass
print(Document.created_at)Built-in Decorators
Python provides several built-in decorators for common use cases. The @property decorator creates managed attributes with getter, setter, and deleter methods, @staticmethod defines methods not requiring instance or class access, @classmethod creates methods receiving the class as the first argument, and @functools.lru_cache provides automatic memoization. Understanding these built-in decorators enables writing cleaner, more Pythonic code following established patterns.
# Built-in Decorators
from functools import lru_cache, wraps
# === @property decorator ===
class Person:
def __init__(self, first_name, last_name):
self._first_name = first_name
self._last_name = last_name
@property
def full_name(self):
"""Computed property."""
return f"{self._first_name} {self._last_name}"
@property
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
if not value:
raise ValueError("First name cannot be empty")
self._first_name = value
@first_name.deleter
def first_name(self):
print("Deleting first_name")
del self._first_name
person = Person("Alice", "Smith")
print(person.full_name) # Alice Smith (computed)
print(person.first_name) # Alice
person.first_name = "Bob" # Uses setter
print(person.full_name) # Bob Smith
# del person.first_name # Uses deleter
# === @staticmethod decorator ===
class MathUtils:
@staticmethod
def add(a, b):
"""Static method - no self or cls."""
return a + b
@staticmethod
def multiply(a, b):
return a * b
# Call without instance
print(MathUtils.add(5, 3)) # 8
print(MathUtils.multiply(4, 7)) # 28
# Can also call on instance
utils = MathUtils()
print(utils.add(2, 3)) # 5
# === @classmethod decorator ===
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
"""Alternative constructor."""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day)
@classmethod
def today(cls):
"""Factory method."""
import datetime
now = datetime.date.today()
return cls(now.year, now.month, now.day)
def __str__(self):
return f"{self.year}-{self.month:02d}-{self.day:02d}"
# Use class methods
date1 = Date(2024, 3, 15)
date2 = Date.from_string("2024-12-25")
date3 = Date.today()
print(date1) # 2024-03-15
print(date2) # 2024-12-25
# === @lru_cache decorator ===
@lru_cache(maxsize=128)
def fibonacci(n):
"""Fibonacci with memoization."""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55
print(fibonacci(20)) # 6765 (very fast due to caching)
# Cache info
print(fibonacci.cache_info())
# CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)
# Clear cache
fibonacci.cache_clear()
# === @lru_cache with arguments ===
@lru_cache(maxsize=None) # Unlimited cache
def expensive_computation(n):
"""Expensive operation."""
import time
time.sleep(0.1)
return n ** 2
print(expensive_computation(5)) # Slow first time
print(expensive_computation(5)) # Instant second time
# === Combining decorators ===
class Calculator:
def __init__(self):
self.history = []
@property
def last_result(self):
"""Get last result from history."""
return self.history[-1] if self.history else None
@staticmethod
def validate_numbers(a, b):
"""Validate inputs."""
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
raise TypeError("Arguments must be numbers")
@classmethod
def create_simple(cls):
"""Factory method."""
return cls()
calc = Calculator.create_simple()
Calculator.validate_numbers(5, 3)Practical Decorator Applications
Decorators solve real-world problems including timing function execution for performance profiling, logging function calls for debugging, implementing authentication and authorization checks, caching expensive computations, validating inputs, handling exceptions with retry logic, rate limiting API calls, and deprecating functions with warnings. These cross-cutting concerns benefit from decorator's declarative syntax separating aspect logic from business logic while enabling consistent application across multiple functions.
# Practical Decorator Applications
from functools import wraps
import time
import warnings
# === 1. Timing decorator ===
def timing_decorator(func):
"""Measure function execution time."""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timing_decorator
def slow_operation():
time.sleep(1)
return "Done"
result = slow_operation()
# Output: slow_operation took 1.0000s
# === 2. Debug logging decorator ===
def debug(func):
"""Print function calls for debugging."""
@wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"Calling {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@debug
def add(a, b):
return a + b
result = add(3, 5)
# Output:
# Calling add(3, 5)
# add returned 8
# === 3. Authentication decorator ===
class AuthenticationError(Exception):
pass
def require_auth(func):
"""Require authentication."""
@wraps(func)
def wrapper(*args, **kwargs):
# Simulate checking authentication
user = kwargs.get('user')
if not user or not user.get('authenticated'):
raise AuthenticationError("Authentication required")
return func(*args, **kwargs)
return wrapper
@require_auth
def view_profile(user=None):
return f"Profile for {user['name']}"
# This raises AuthenticationError:
# view_profile()
# This works:
result = view_profile(user={'name': 'Alice', 'authenticated': True})
print(result)
# === 4. Caching decorator ===
def cache(func):
"""Simple caching decorator."""
cached_results = {}
@wraps(func)
def wrapper(*args):
if args in cached_results:
print(f"Cache hit for {args}")
return cached_results[args]
print(f"Computing for {args}")
result = func(*args)
cached_results[args] = result
return result
return wrapper
@cache
def expensive_function(n):
time.sleep(0.5)
return n ** 2
print(expensive_function(5)) # Computing for (5,)
print(expensive_function(5)) # Cache hit for (5,)
# === 5. Input validation decorator ===
def validate_positive(func):
"""Ensure all arguments are positive."""
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(f"All arguments must be positive, got {arg}")
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(width, height):
return width * height
print(calculate_area(5, 10)) # 50
# calculate_area(-5, 10) # ValueError
# === 6. Retry decorator ===
def retry(max_attempts=3):
"""Retry function on exception."""
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
print(f"Attempt {attempt + 1} failed: {e}")
return None
return wrapper
return decorator
@retry(max_attempts=3)
def unreliable_api_call():
import random
if random.random() < 0.7:
raise ConnectionError("API unavailable")
return "Success"
# === 7. Deprecation warning ===
def deprecated(replacement=None):
"""Mark function as deprecated."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
message = f"{func.__name__} is deprecated"
if replacement:
message += f", use {replacement} instead"
warnings.warn(message, DeprecationWarning, stacklevel=2)
return func(*args, **kwargs)
return wrapper
return decorator
@deprecated(replacement="new_function")
def old_function():
return "Old behavior"
# result = old_function() # Prints deprecation warning
# === 8. Singleton decorator ===
def singleton(cls):
"""Make class a singleton."""
instances = {}
@wraps(cls)
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class Database:
def __init__(self):
print("Creating database connection")
db1 = Database() # Creating database connection
db2 = Database() # Returns same instance
print(db1 is db2) # TrueBest Practices
- Always use @wraps: Import and use
functools.wrapsto preserve the decorated function's metadata including name, docstring, and signature - Accept *args and **kwargs: Wrapper functions should accept arbitrary arguments using
*args, **kwargsto work with any function signature - Preserve return values: Ensure decorators return the original function's result unless intentionally modifying it
- Keep decorators simple: Each decorator should have a single, clear purpose. Complex logic belongs in separate functions or classes
- Document decorator behavior: Clearly document what decorators do, especially if they modify function behavior or add requirements
- Consider class-based for state: Use class-based decorators when maintaining state like call counts or caches. Use functions for stateless decorators
- Test decorated and undecorated: Test both the decorator itself and decorated functions. Consider testing functions without decorators for unit tests
- Be mindful of performance: Decorators add overhead. Avoid heavy operations in wrapper functions called frequently
- Order matters with stacking: When stacking decorators, remember they're applied bottom-up. Test decorator combinations carefully
- Use built-ins when available: Prefer
@property,@staticmethod,@classmethod, and@lru_cacheover custom implementations
@wraps(func), 2) Accept *args, **kwargs, 3) Return the function's result, 4) Have clear documentation. Follow these rules for maintainable decorators.Conclusion
Python decorators provide powerful metaprogramming capabilities enabling function and class behavior modification without altering source code, leveraging first-class function support where functions can be passed as arguments, returned from functions, and assigned to variables. Basic decorator syntax defines wrapper functions receiving the original function, executing code before and after calling it, and using @ syntax for elegant application at definition time. The functools.wraps decorator preserves original function metadata including name, docstring, and signature preventing loss during wrapping, while multiple decorators can stack applying transformations bottom-up enabling behavior composition. Decorators accepting arguments require decorator factories adding an extra nesting level, with the outer function receiving decorator arguments, middle function receiving the decorated function, and inner function providing the wrapper logic.
Class-based decorators use __init__() receiving decorated functions and __call__() executing wrapper logic, enabling stateful decorators maintaining call counts, caches, or execution history through instance variables while organizing complex logic into methods rather than nested functions. Built-in decorators include @property creating managed attributes with getters, setters, and deleters, @staticmethod defining methods not requiring instance access, @classmethod creating methods receiving classes as first arguments useful for factory methods, and @lru_cache providing automatic memoization with configurable cache sizes. Practical applications demonstrate decorators' value for timing function execution profiling performance, logging calls for debugging, implementing authentication and authorization checks, caching expensive computations avoiding redundant work, validating inputs ensuring correct data, handling exceptions with retry logic for resilience, rate limiting API calls preventing abuse, and deprecating functions with warnings guiding migrations. Best practices emphasize always using @wraps preserving metadata, accepting *args and **kwargs for flexibility, preserving return values unless intentionally modifying, keeping decorators simple with single purposes, documenting behavior clearly, preferring class-based decorators for stateful logic, testing decorated and undecorated functions separately, being mindful of performance overhead, understanding stacking order, and using built-in decorators over custom implementations. By mastering decorator fundamentals with function wrapping and @ syntax, decorator factories for parameterization, functools.wraps for metadata preservation, class-based decorators for statefulness, built-in decorators for common patterns, practical applications solving cross-cutting concerns, and best practices ensuring maintainable implementations, you gain essential tools for writing clean, reusable Python code that separates concerns through composable behavior modification enabling elegant solutions to logging, caching, validation, authentication, and other aspects spanning multiple functions in professional software development.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


