Metaprogramming Advanced
Metaprogramming is not just a bag of tricks. It is a design tool for reducing repetition and exposing declarative APIs. The key is cost-aware tool selection: choose the smallest hook that solves the problem cleanly.
Quick takeaway: if two hooks can solve the same problem, pick the simpler one. In practice, the ladder is descriptor / `__set_name__` -> `__init_subclass__` -> class decorator -> metaclass -> import hook. The success criterion is not "more magic"; it is lower maintenance cost with clearer intent.
Hook Selection Ladder
1) __set_name__: when field-name binding matters
py
class Positive:
def __set_name__(self, owner: type, name: str) -> None:
self.private_name = f"_{name}"
def __get__(self, instance: object | None, owner: type | None = None) -> object:
if instance is None:
return self
return getattr(instance, self.private_name)
def __set__(self, instance: object, value: int) -> None:
if value <= 0:
raise ValueError("must be positive")
setattr(instance, self.private_name, value)1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Use it for:
- field-level access policy
- validation/caching/lazy binding
- descriptor behavior that depends on class attribute names
2) __init_subclass__: enforce or register subclass contracts
py
class HandlerBase:
registry: dict[str, type["HandlerBase"]] = {}
def __init_subclass__(cls, *, key: str | None = None, **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
if key is not None:
HandlerBase.registry[key] = cls
class JsonHandler(HandlerBase, key="json"):
pass1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Use it for:
- plugin registration
- subclass validation
- class-level policy without full metaclass cost
3) Class decorators: preserve declaration ergonomics, apply post-processing
py
from dataclasses import dataclass
def service(name: str):
def decorator(cls: type) -> type:
cls.service_name = name
return dataclass(slots=True)(cls)
return decorator
@service("billing")
class BillingConfig:
retries: int = 31
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Use it for:
- post-processing class objects
- registration plus metadata injection
- combining with dataclass/attrs/pydantic style decorators
4) Metaclasses: only when class creation policy must be controlled
py
class NoMutableDefaults(type):
def __new__(
mcls,
name: str,
bases: tuple[type, ...],
namespace: dict[str, object],
) -> type:
for key, value in namespace.items():
if isinstance(value, (list, dict, set)):
raise TypeError(f"{name}.{key} has mutable class default")
return super().__new__(mcls, name, bases, namespace)1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
Use it for:
- class-body DSL interpretation
- hard class-creation policy
- cases where decorators and
__init_subclass__are insufficient
5) Import hooks and AST transforms: only for strong global needs
- highest cost, highest surprise potential
- impacts debugging and tooling significantly
- avoid unless there is a team-wide policy reason
Common Open-source Shapes
- Click/FastAPI: declarative decorators delegating to core registration functions
- SQLAlchemy: descriptor plus class-construction hook composition
- Pydantic: thin public methods delegating to internal validator engines
See Open-source Pythonic Deep Dives for concrete code excerpts.
Review Checklist
- Could a lower-cost hook solve this?
- Is runtime behavior explicit at boundaries (inputs, outputs, errors)?
- Can a new team member read and modify this safely?
- Is test substitution (fakes/mocks) straightforward?
- Is the hook choice documented with rationale?
Runnable Labs in This Repository
examples/metaprogramming_hooks_lab.pyexamples/usecase_with_uow_abc.py