Warming up the neural circuits...
By the end of this chapter you will:
Descriptors and metaclass-level hooks power many frameworks: ORMs, systems, plugin registries, and declarative APIs.
Most application code should not start here. But if you want to understand framework internals and build clean extension systems, this chapter is critical.
class User:
pass
print(type(User)) # undefined
print(type(type)) # undefinedtype is the metaclass that creates classes by default.
class Positive:
def __set_name__(self, owner, name):
self.private_name = f"_{name}"
def __get__(self, obj, objtype=None):
if
class Product:
price = Positive()
def __init__(self, price: float):
self.price = price
p = Product(100)
# p.price = -5 -> ValueErrorA single descriptor can enforce rules for many models.
class Plugin:
registry: dict[str, type] = {}
def __init_subclass__(cls, name: str, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.
class RequirePrefixMeta(type):
def __new__(mcls, name, bases, namespace):
if not name.startswith("X"):
raise TypeError("class name must start with X")
return superUse descriptors and metaclass hooks to build reusable framework primitives, not to make business features harder to read.
| Need | Use |
|---|---|
| Field-level access/validation policy | Descriptor |
| Subclass auto-registration | __init_subclass__ |
| Class-creation constraints | Custom metaclass |
| Simple computed attribute | @property |
| Mistake | Symptom | Fix |
|---|---|---|
| Jumping directly to metaclass for simple registration | Over-engineered code | Start with __init_subclass__ |
| Descriptor storing on descriptor instance only | Shared cross-instance bugs | Store per-instance state on object using private names |
Forgetting obj is None branch in __get__ | Class-level attribute access breaks | Return descriptor itself for class access |
| Hiding business logic in metaclass side effects | Debugging pain | Keep logic explicit and test hooks thoroughly |
| Using these patterns without documentation | Team confusion | Add clear usage examples and invariants |
type.Simple class Usertype(User) is typeModel field amountValueError on invalid assignmentBase Parser classRegistry map filled automatically10 models share same validationDescriptor preferred for reusable policyFramework design scenarioBalanced, non-hype evaluationBeginner:
"What is a descriptor in Python?"
An object implementing
__get__,__set__, or__delete__that controls attribute access behavior.
"When should you use init_subclass?"
When you need subclass registration or lightweight class-creation hooks without full metaclass complexity.
Senior:
"How do ORMs use descriptors?"
Field objects often act as descriptors to intercept reads/writes and route behavior through validation, conversion, and persistence layers.
"What is your bar for introducing a custom metaclass?"
Only when class-creation behavior must be centralized and cannot be expressed cleanly via decorators, descriptors, or
__init_subclass__.
__init_subclass__ solves many registration needs without metaclasses.Need reusable field policy? -> descriptor
Need subclass registration? -> __init_subclass__
Need class creation enforcement? -> metaclass
Need one-class computed field? -> @propertyWhat is type(User) for a normal Python class User?
Descriptors intercept access with reusable policy.
This is often enough; full custom metaclasses are rarely required.
Reserve this power for framework-level constraints.