the full guide

Metaclasses and descriptors

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.

12 min Advanced

What you need: The earlier topics of this module, and its prerequisites.

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

meta.py
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

Question 1

1. What is the default metaclass?

Question 2

2. Which built-in is implemented with descriptors?

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