Decorators
Master Python decorators — function wrappers, parameterized decorators, class decorators, and functools.wraps for cross-cutting concerns.
Decorators modify or enhance functions and classes without changing their source code. They’re used for logging, timing, authentication, caching, and more.
Functions as First-Class Objects
Functions can be passed as arguments and returned from other functions:
def greet(name):
return f"Hello, {name}!"
def shout(func):
def wrapper(name):
result = func(name)
return result.upper()
return wrapper
loud_greet = shout(greet)
loud_greet("Alice") # "HELLO, ALICE!"
Basic Decorator Syntax
@decorator is syntactic sugar for reassigning the function:
def timer(func):
import time
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function():
import time
time.sleep(1)
return "done"
slow_function() # slow_function took 1.0012s
Preserving Metadata with functools.wraps
Decorators replace the original function, losing its name and docstring:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper docstring — not what we want exposed."""
return func(*args, **kwargs)
return wrapper
@my_decorator
def important_function():
"""This docstring should be preserved."""
pass
print(important_function.__name__) # important_function
print(important_function.__doc__) # This docstring should be preserved.
Always use @functools.wraps(func) in decorators.
Decorators with Arguments
Create decorator factories when you need configurable behavior:
import functools
import time
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.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=2)
def fetch_data():
...
Built-in Decorators
@property — Computed Attributes
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius ** 2
@property
def diameter(self):
return self.radius * 2
c = Circle(5)
print(c.area) # 78.54 — accessed like an attribute
print(c.diameter) # 10
@staticmethod and @classmethod
class Date:
def __init__(self, year, month, day):
self.year, self.month, self.day = year, month, day
@classmethod
def from_string(cls, date_str):
year, month, day = map(int, date_str.split("-"))
return cls(year, month, day)
@staticmethod
def is_leap_year(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
d = Date.from_string("2024-06-15")
Date.is_leap_year(2024) # True
Class Decorators
def singleton(cls):
instances = {}
@functools.wraps(cls)
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("Connecting...")
db1 = Database() # Connecting...
db2 = Database() # (no output — same instance)
assert db1 is db2
Stacking Decorators
Decorators apply bottom-up:
@decorator_a
@decorator_b
def func():
pass
# Equivalent to: func = decorator_a(decorator_b(func))
Practical Examples
Access Control
def require_auth(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("authenticated"):
raise PermissionError("Login required")
return func(user, *args, **kwargs)
return wrapper
Caching
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
Decorators are everywhere in Python frameworks — Flask routes (@app.route), pytest fixtures (@pytest.fixture), and dataclass generation (@dataclass) all use them.