Python Decorators
Elegantly modify and enhance functions and classes
What are Decorators?
Decorators are a powerful Python feature that allows you to modify or enhance functions and classes without changing their source code. They use the @syntax to wrap functions, adding functionality before, after, or around the original code.
Basic Function Decorators
A decorator is a function that takes another function as an argument and returns a new function with added behavior.
def my_decorator(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
# Output:
# Before function call
# Hello!
# After function call@decorator syntax is syntactic sugar for func = decorator(func)Decorators with Function Arguments
Use *args and **kwargs to make decorators work with any function signature.
def log_args(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with:")
print(f" args: {args}")
print(f" kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f" returned: {result}")
return result
return wrapper
@log_args
def add(a, b):
return a + b
print(add(5, 3))*args, **kwargs in wrapper to maintain flexibilityPreserving Function Metadata
Use functools.wraps to preserve the original function's metadata.
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def complex_function():
"""This function does something complex"""
pass
print(complex_function.__name__) # 'complex_function'
print(complex_function.__doc__) # Original docstring@wraps(func) to avoid debugging headachesDecorators with Parameters
Create decorators that accept arguments by adding another level of nesting.
from functools import wraps
def repeat(times):
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")
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!params → decorator → wrapper → functionPractical Decorator Examples
Common real-world use cases for decorators.
import time
from functools import wraps
# Timing decorator
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "Done"
# Memoization decorator
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)Class Decorators
Decorators can also modify or enhance entire classes.
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
print("Creating database connection")
db1 = Database()
db2 = Database()
print(db1 is db2) # TrueBuilt-in Method Decorators
Python provides several built-in decorators for class methods.
class MyClass:
def __init__(self, value):
self._value = value
@classmethod
def from_string(cls, string):
value = int(string)
return cls(value)
@staticmethod
def static_method():
return "Static method called"
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
if new_value < 0:
raise ValueError("Must be positive")
self._value = new_value
obj = MyClass(10)
print(obj.value) # 10
obj.value = 20 # Uses setterChaining Multiple Decorators
Stack multiple decorators on a single function. Applied bottom-to-top.
def uppercase(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def exclaim(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + "!"
return wrapper
@uppercase
@exclaim
def greet(name):
return f"hello, {name}"
print(greet("Alice")) # "HELLO, ALICE!"Key Takeaways
- Decorators modify functions without changing source code
@decoratoris sugar forfunc = decorator(func)- Always use
@wraps(func)to preserve metadata - Use
*args, **kwargsfor flexibility - Parameterized decorators require extra nesting level
- Common uses: timing, caching, authentication, logging
- Class decorators can modify entire classes
- Multiple decorators applied bottom-to-top
Bonus: Useful Decorator Libraries
core-mixins is a Python library providing common functions, decorators, and mixin classes for professional Python development.
The library includes:
- Caching decorators - Built-in memoization and result caching
- Performance monitoring - Timing and profiling decorators
- Retry logic - Automatic retry with backoff strategies
- Common mixins - Reusable class mixins for extending functionality
Other popular decorator libraries:
functools- Built-in utilities (lru_cache, wraps)decorator- Simplifies decorator creationwrapt- Advanced decorator library with proper signature preservationtenacity- Flexible retry logic with multiple strategies
Quick Reference
| Type | Syntax | Use Case |
|---|---|---|
| Basic | @decorator | Wrap function behavior |
| With Parameters | @decorator(args) | Configurable decorators |
| Class Decorator | @decorator class X | Modify entire classes |
| @property | @property def x | Getter as attribute |
| @classmethod | @classmethod def x | Class-level methods |
| @staticmethod | @staticmethod def x | Utility functions |
Practice Exercise
Challenge: Create a @validate decorator that checks function arguments against validation rules and raises ValueError if validation fails.
Show Solution
from functools import wraps
import inspect
def validate(**validators):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for param, validator in validators.items():
if param in bound.arguments:
value = bound.arguments[param]
if not validator(value):
raise ValueError(
f"Validation failed for {param}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate(
age=lambda x: 18 <= x <= 100,
name=lambda x: len(x) > 0
)
def create_user(name, age):
return f"User {name} ({age}) created"
print(create_user("Alice", 25))What's Next?
You've learned the power of decorators! Next, we'll explore how to work with different file formats and serialize data.
- JSON, CSV, XML - Master reading and writing structured data formats
- Data Serialization - Convert Python objects to storable formats and back
- Binary Files - Work with pickle and other binary serialization methods