Descriptors and Metaclasses
How frameworks work their magic.
Frameworks like Django, and pydantic, seem magical. You write a class with a few fields, and validation, and database columns appear. Behind the scenes, they use two advanced features. Descriptors control how attributes are read and written. Metaclasses control how classes themselves are created. Understanding them demystifies a lot of Python.

- Frameworks seem magical
- Descriptors: control attribute access
- Metaclasses: control class creation
A descriptor for validation
A descriptor is an object with a get and a set method, placed on a class. When you read or assign the attribute, Python calls those methods. Properties are built on this idea. Here, a Positive descriptor, rejects negative values, and can be reused on any attribute, of any class.
class Positive:
def __set_name__(self, owner, name):
self.name = "_" + name
def __get__(self, obj, cls):
return getattr(obj, self.name)
def __set__(self, obj, value):
if value < 0:
raise ValueError("must be positive")
setattr(obj, self.name, value)
class Plant:
price = Positive()
stock = Positive()
def __init__(self, price, stock):
self.price, self.stock = price, stock
p = Plant(9.5, 3)
print(p.price, p.stock)
try:
p.price = -1
except ValueError as e:
print(e)9.5 3 must be positive
Everything is an object, including classes
In Python, classes are objects too. The type of an object is its class, and the type of a class is, by default, type. You can even create a class by calling type, with a name, bases, and a dictionary of attributes. This is exactly what the class statement does for you.
class Plant:
kind = "green"
print(type(Plant))
Pot = type("Pot", (), {"kind": "clay", "size": lambda self: 5})
p = Pot()
print(p.kind, p.size(), type(p).__name__)<class 'type'> clay 5 Pot
A metaclass that registers classes
A metaclass is a class whose instances are classes. By customising it, you can run code every time a class is defined. Here, a registry metaclass records every Plugin subclass in a dictionary. Frameworks use this to discover your models, plugins, and handlers, automatically.
REGISTRY = {}
class Meta(type):
def __new__(mcls, name, bases, ns):
cls = super().__new__(mcls, name, bases, ns)
if bases:
REGISTRY[name.lower()] = cls
return cls
class Plugin(metaclass=Meta):
pass
class Fern(Plugin): pass
class Ivy(Plugin): pass
print(sorted(REGISTRY))
print(REGISTRY["fern"] is Fern)['fern', 'ivy'] True
__init_subclass__: the simple alternative
Metaclasses are powerful, but rarely needed. A simpler hook, called init subclass, runs whenever a subclass is created, and covers most use cases. Use it first. The golden rule of metaclasses is, if you wonder whether you need one, you do not.
class Plugin:
registry = []
def __init_subclass__(cls, **kw):
super().__init_subclass__(**kw)
Plugin.registry.append(cls.__name__)
class Fern(Plugin): pass
class Ivy(Plugin): pass
print(Plugin.registry)['Fern', 'Ivy']
A tracking class
Your turn. Use init subclass in a base class called Shape, to count how many subclasses are defined. Define two subclasses, and print the count. Ready? Here is one solution. A class level counter, increased inside the hook.
Use __init_subclass__ in Shape to count subclasses; define two and print the count.
Show hint
increment Shape.count in the hook
Show solution
class Shape:
count = 0
def __init_subclass__(cls, **kw):
super().__init_subclass__(**kw)
Shape.count += 1
class Circle(Shape): pass
class Square(Shape): pass
print(Shape.count)2
Summary
Excellent. Descriptors control attribute access and power properties and validation. Classes are objects, created by type, and metaclasses customise that creation, although init subclass is usually enough. Next, we finish with design patterns, clean code, and the habits of top Python developers.
Key takeaways- Descriptors: __get__, __set__, __set_name__
- Classes are objects created by type
- Prefer __init_subclass__ to metaclasses