Metaprogramming
Explore Python metaprogramming — descriptors, property decorators, metaclasses, getattr, and dynamic code generation for advanced library design.
Metaprogramming is code that manipulates code — creating classes at runtime, intercepting attribute access, or building DSLs. Python’s dynamic nature makes it unusually powerful here.
Descriptors
Descriptors control attribute access via __get__, __set__, and __delete__:
class Validator:
def __init__(self, min_value, max_value):
self.min_value = min_value
self.max_value = max_value
def __set_name__(self, owner, name):
self.name = f"_{name}"
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.name)
def __set__(self, obj, value):
if not (self.min_value <= value <= self.max_value):
raise ValueError(f"{self.name} must be between {self.min_value} and {self.max_value}")
setattr(obj, self.name, value)
class Product:
price = Validator(0, 10000)
quantity = Validator(0, 1000)
def __init__(self, price, quantity):
self.price = price
self.quantity = quantity
p = Product(29.99, 10)
p.price = -5 # ValueError
@property is implemented using descriptors under the hood.
Dynamic Attribute Access
class DynamicObject:
def __init__(self, **kwargs):
self._data = kwargs
def __getattr__(self, name):
if name in self._data:
return self._data[name]
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
def __setattr__(self, name, value):
if name == "_data":
super().__setattr__(name, value)
else:
self._data[name] = value
obj = DynamicObject(name="Alice", age=30)
print(obj.name) # Alice
Metaclasses
Metaclasses are “classes of classes” — they control class creation:
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
pass
db1 = Database()
db2 = Database()
assert db1 is db2
Use metaclasses sparingly — they add complexity. Often a decorator or __init_subclass__ is cleaner.
init_subclass — Register Subclasses
A Pythonic alternative to metaclasses for plugin systems:
class PluginBase:
registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
PluginBase.registry[cls.__name__] = cls
class EmailPlugin(PluginBase):
pass
class SlackPlugin(PluginBase):
pass
print(PluginBase.registry)
# {'EmailPlugin': <class 'EmailPlugin'>, 'SlackPlugin': <class 'SlackPlugin'>}
Decorators as Metaprogramming
Decorators modify functions at definition time:
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:
if attempt == max_attempts - 1:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=2)
def fetch_data():
...
exec and eval (Use with Caution)
# eval — evaluate an expression
result = eval("2 + 3 * 4") # 14
# exec — execute statements
namespace = {}
exec("x = 42\ny = x * 2", namespace)
print(namespace["y"]) # 84
Never use eval/exec on untrusted input — it’s a security risk.
When to Use Metaprogramming
- ORM field definitions (Django models, SQLAlchemy columns)
- Validation frameworks (Pydantic, attrs)
- Plugin architectures
- API client generators
Avoid metaprogramming when plain functions and classes would be clearer. The best metaprogramming is invisible to the user of your library.