Python Decorators and Metaclasses: Advanced Patterns
Going beyond @property iting decorators with arguments, class decorators, and when metaclasses are actually the right tool.
Decorators: Functions That Return Functions
At its core, a decorator replaces a function with a wrapper:
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 heavy_computation(n):
return sum(i**2 for i in range(n))
Decorators with Arguments
This requires an extra layer of nesting:
def retry(max_attempts=3, delay=1.0):
def decorator(func):
import time
from functools import wraps
@wraps(func) # Preserves __name__, __doc__
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
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=5, delay=2.0)
def fetch_data(url):
...
Class Decorators
Decorators can also modify 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 DatabaseConnection:
def __init__(self, url):
self.url = url
self.connect()
Metaclasses: Classes That Create Classes
A metaclass controls how a class itself is created. Every class in Python is an instance of a metaclass (usually type).
class ValidatedMeta(type):
def __new__(mcs, name, bases, namespace):
# Enforce that all methods have docstrings
for key, value in namespace.items():
if callable(value) and not key.startswith('_'):
if not value.__doc__:
raise TypeError(
f"Method {name}.{key} must have a docstring"
)
return super().__new__(mcs, name, bases, namespace)
class APIEndpoint(metaclass=ValidatedMeta):
def get(self, request):
"""Handle GET requests."""
...
def post(self, request): # TypeError! No docstring
...
When to Use Metaclasses
Almost never. But legitimate use cases:
- ORM field registration (Django models)
- Abstract base class enforcement (abc.ABCMeta)
- Automatic registration (plugin systems)
- API validation at class definition time
The Hierarchy
type → metaclass → class → instance
type("MyClass", (Base,), {"method": fn}) == class MyClass(Base): ...
Prefer __init_subclass__ (Python 3.6+)
For most metaclass use cases, __init_subclass__ is simpler:
class Plugin:
registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.registry[cls.__name__] = cls
class ImageProcessor(Plugin):
...
class VideoProcessor(Plugin):
...
print(Plugin.registry)
# {'ImageProcessor': <class 'ImageProcessor'>, 'VideoProcessor': <class 'VideoProcessor'>}
Rule of thumb: decorators > __init_subclass__ > metaclasses. Use the simplest tool that solves the problem.