AI Hints
Home›Python Tutorial›Under the Hood: Performance and Memory
Python · Lesson 43 of 49

Descriptors and Metaclasses

How frameworks work their magic.

⏱ 2 min read▶ Video at 2:09:56🎓 Free
Watch this lesson in the Python courseWatch on YouTube ↗Full Python course

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.

Descriptors and Metaclasses
From the video: Descriptors and Metaclasses
  • 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.

descriptor.pypython
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)
Output
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.

type1.pypython
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__)
Output
<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.

meta.pypython
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)
Output
['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.

init_sub.pypython
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)
Output
['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.

✍️ Exercise: A tracking class

Use __init_subclass__ in Shape to count subclasses; define two and print the count.

Show hint

increment Shape.count in the hook

Show solution
shape_count.pypython
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)
Output
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
Next up: Clean Code and Patterns — habits of top Python developers