Definition
A descriptor is an object that controls attribute access, through __get__, __set__ and __delete__. It's how property works. A metaclass is the class of a class: it controls how classes themselves are created. The default metaclass is type.
In simple words
Objects are made from classes, and classes are made from metaclasses. You'll rarely write these, but they explain how frameworks like Django and Pydantic work their magic.
Why it matters
Knowing they exist helps you read advanced library code. For most needs, __init_subclass__ or a class decorator is simpler.
How it works
In code
class Positive: # a descriptor
def __set_name__(self, owner, name):
self.name = "_" + name
def __get__(self, obj, owner=None):
return getattr(obj, self.name)
def __set__(self, obj, value):
if value <= 0:
raise ValueError("must be positive")
setattr(obj, self.name, value)
class Item:
price = Positive()
def __init__(self, price):
self.price = price
print(Item(5).price)
try:
Item(-1)
except ValueError as error:
print("Refused:", error)
class Registry(type): # a metaclass
classes = []
def __new__(mcls, name, bases, body):
cls = super().__new__(mcls, name, bases, body)
Registry.classes.append(name)
return cls
class ModelA(metaclass=Registry): ...
class ModelB(metaclass=Registry): ...
print(Registry.classes)
print(type(int), type(ModelA).__name__)Output
5 Refused: must be positive ['ModelA', 'ModelB'] <class 'type'> Registry
Common mistakes
- Using a metaclass when a decorator or __init_subclass__ would do.
- Storing a descriptor's value on the descriptor itself. All objects would then share it. Store it on obj.
Check yourself
Summary
- Descriptors control attribute access.
- Metaclasses control how classes are created.
- You'll read them more often than write them.
Finished reading?
Sources: Python docs: Descriptor HowTo