36  Class Metaprogramming

NoteCore idea

Classes are objects. type() is the metaclass of all classes. You can create classes dynamically, customize class creation with __init_subclass__, or build metaclasses for framework-level control. These are powerful tools — use them sparingly.

In this chapter you will learn to:

  1. Confirm that classes are first-class objects whose class is type.
  2. Create a class dynamically with type(name, bases, dict).
  3. Customize subclass creation with __init_subclass__ (PEP 487).
  4. Distinguish “import time” from “call time” — the moment when class bodies and decorators run.
  5. Write a metaclass and recognize when (rarely!) it’s the right tool.

36.1 Classes as objects

Every class in Python is an instance of type. That includes built-ins:

int.__class__, str.__class__, type.__class__
(type, type, type)

The output is (<class 'type'>, <class 'type'>, <class 'type'>). Each of int, str, and type itself is an instance of type — that’s the meaning of “type is the metaclass of all classes.” The recursive type.__class__ is type is the bootstrap fixed point: the class-of-a-class chain has to end somewhere, and Python ends it by making type its own class.

type is its own metaclass — that’s how the system bootstraps. Classes carry the same kind of attributes any other object does:

class Foo: ...
Foo.__name__, Foo.__mro__, Foo.__module__
('Foo', (__main__.Foo, object), '__main__')

Foo.__name__ is 'Foo' (the source-level name). Foo.__mro__ is the method-resolution order, a tuple (Foo, object) because Foo only inherits from object (the implicit default). Foo.__module__ is '__main__' in this notebook context, or whatever module the class was defined in. These are just attributes on the class object — exactly the same kind of thing as Foo.method would be — because classes are objects.

You can pass classes around, store them in lists and dicts, and pick one at runtime to instantiate. None of that is “metaprogramming” — it’s just the data model.

The class-of-a-class chain bottoms out at type, which is its own metaclass:

flowchart LR
    I[instance] -->|__class__| C[class]
    C -->|__class__| M[metaclass]
    M -->|__class__| T[type]
    T -->|__class__| T

PEP 695’s class C[T]: syntax (Python 3.12) uses an internal metaclass mechanism to bind the type parameters — it’s there, but you never see it.

36.2 type as a class factory

If classes are objects, something must build them. That something is type itself, used in its three-argument form. You’ll rarely write this by hand — but every class statement in your code goes through it under the hood, and frameworks that generate classes at runtime call it directly.

MyClass = type("MyClass", (object,), {"x": 1, "method": lambda self: self.x})
MyClass.__name__, MyClass().method()
('MyClass', 1)

Walking through the three arguments:

  • "MyClass" is the class name — what shows up in __name__ and tracebacks.
  • (object,) is the tuple of bases — the parents to inherit from. A single-element tuple needs the trailing comma.
  • {"x": 1, "method": ...} is the namespace dict — the equivalent of the class body. Anything you’d put in a class block goes here as a key/value pair.

That’s exactly equivalent to:

class MyClass(object):
    x = 1
    def method(self):
        return self.x

The general insight: the class statement is syntax sugar over type(name, bases, dict). Frameworks that generate classes (Django models, SQLAlchemy mappers) use this form internally — the user writes a declarative spec, the framework calls type(...) to build the actual class.

36.3 When the class body runs

Class bodies run at import time, not at call time. Decorators on classes also run at import time. This is what makes registry decorators useful, and what makes metaclasses occasionally surprising:

class Watcher:
    print("class body executing")

class Watched(Watcher):
    print("subclass body executing")

print("module loaded")
class body executing
subclass body executing
module loaded

When the cell is run, both class bodies execute. There is no “instantiate to run” step for the class body — the body runs once, and the result is the class object.

36.4 __init_subclass__ (PEP 487)

The modern alternative to a metaclass for many use cases. __init_subclass__ is a classmethod (implicitly) that’s called on the parent whenever a subclass is created. Three small steps show how far it stretches.

Step 1: register every subclass. The simplest hook just records the new class:

class Pluggable:
    plugins = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Pluggable.plugins.append(cls)
        print(f"registered {cls.__name__}")

class A(Pluggable): ...
class B(Pluggable): ...

[c.__name__ for c in Pluggable.plugins]
registered A
registered B
['A', 'B']

Each subclass declaration triggered the registration. No metaclass; no decorator on each subclass; just a single hook on the base.

Step 2: inspect the subclass’s annotations. cls.__annotations__ is the dict of name-to-type from the subclass body. Reading it inside __init_subclass__ lets the parent react to the subclass’s schema:

class Annotated:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for name, type_ in cls.__annotations__.items():
            print(f"{cls.__name__}.{name}: {type_.__name__}")

class Person(Annotated):
    name: str
    age: int
Person.name: str
Person.age: int
  • cls.__annotations__ collects the {name: type} pairs from Person’s body. The hook runs after the body executes, so the dict is already populated.
  • The values are the type objects themselves (str, int), not strings — you can issubclass against them, instantiate them, anything you’d do with a class.

Step 3: combine with descriptors for an ORM-like model. The user writes a clean class with type annotations; the framework reads those annotations and installs validating descriptors automatically — no decorator on every field, no metaclass:

import abc

class Validated(abc.ABC):
    def __set_name__(self, owner, name):
        self.private = "_" + name
    def __get__(self, instance, owner=None):
        if instance is None: return self
        return getattr(instance, self.private)
    def __set__(self, instance, value):
        setattr(instance, self.private, self.validate(value))
    @abc.abstractmethod
    def validate(self, value): ...

class Positive(Validated):
    def validate(self, value):
        if value <= 0:
            raise ValueError(f"expected > 0, got {value!r}")
        return value

class Checked:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for name, type_ in cls.__annotations__.items():
            if isinstance(type_, type) and issubclass(type_, Validated):
                descriptor = type_()
                descriptor.__set_name__(cls, name)
                setattr(cls, name, descriptor)

    def __init__(self, **kwargs):
        for name, value in kwargs.items():
            setattr(self, name, value)

class Movie(Checked):
    title: str
    year: int
    box_office: Positive

m = Movie(title="Godzilla", year=2014, box_office=300_000_000)
m.title, m.box_office
('Godzilla', 300000000)
m.box_office = -1
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[8], line 1
----> 1 m.box_office = -1

Cell In[7], line 10, in Validated.__set__(self, instance, value)
      9     def __set__(self, instance, value):
---> 10         setattr(instance, self.private, self.validate(value))

Cell In[7], line 17, in Positive.validate(self, value)
     15     def validate(self, value):
     16         if value <= 0:
---> 17             raise ValueError(f"expected > 0, got {value!r}")
     18         return value

ValueError: expected > 0, got -1

Walking through how the pieces fit:

  • Validated, Positive — the descriptor base and rule from Section 35.5. Nothing new there.
  • Checked.__init_subclass__ walks cls.__annotations__. For each annotation whose value is a class and a Validated subclass — box_office: Positive qualifies; title: str and year: int do not — it constructs a descriptor instance, calls __set_name__ manually (because we’re installing the descriptor after the class body finished, so Python’s automatic call has already passed), and setattrs it onto the class.
  • __init__ accepts keyword arguments and assigns each — assignments to box_office route through the freshly-installed Positive descriptor, which validates.

The general insight: __init_subclass__ walked cls.__annotations__ and replaced fields whose type is a Validated subclass with descriptor instances. Movie itself needs no metaclass, no decorator, no boilerplate — the parent’s hook does the work.

36.5 Metaclasses, when nothing else fits

__init_subclass__ runs on the parent every time a child is defined — but you have to be the parent. What if you want to intercept class creation for classes that don’t share a base, or you need to change the namespace dict before the class body runs? That’s the metaclass’s job. A metaclass is a class whose instances are classes; the convention is to subclass type. Two steps show the mechanism and why it earns its keep.

Step 1: a minimal metaclass. Override __new__ and you intercept class creation:

class Loud(type):
    def __new__(meta, name, bases, namespace):
        cls = super().__new__(meta, name, bases, namespace)
        print(f"created {name}")
        return cls

class Quiet(metaclass=Loud):
    ...
created Quiet
  • class Loud(type): declares a metaclass — a subclass of type. Its instances will be classes.
  • __new__(meta, name, bases, namespace) is the class-creation constructor. The four arguments are the same ones type(...) takes: the metaclass itself, the new class’s name, its tuple of bases, and its namespace dict. It runs at the moment the new class is being built.
  • super().__new__(meta, name, bases, namespace) delegates to type.__new__ to actually build the class — skip that call and no class comes back.
  • class Quiet(metaclass=Loud): says “use Loud as the class factory for Quiet.” When the class statement executes, Python calls Loud("Quiet", (), {...}) instead of the implicit type("Quiet", (), {...}), so __new__ runs and you see the print.

Step 2: why a metaclass and not __init_subclass__. The distinctive property: classes with a metaclass do not need to share a base. __init_subclass__ requires every managed class to inherit from the parent that defines the hook; a metaclass works through the metaclass= keyword on classes that have nothing else in common:

class A(metaclass=Loud):
    pass

class B(dict, metaclass=Loud):
    pass

[type(A).__name__, type(B).__name__]
created A
created B
['Loud', 'Loud']
  • A has no bases at all; B extends dict. Both still trigger Loud.__new__.
  • type(A) is Loud — that’s the metaclass relationship made visible.
  • A subclass of either A or B will also use Loud as its metaclass — metaclasses inherit. That’s how a single metaclass can manage a whole tree of classes with mixed bases.

The general insight: __new__ on a metaclass runs at class-creation time — a step earlier than __init_subclass__, and applicable to classes that don’t share a base. Reach for one only when the customization needs to run on classes outside a single inheritance line.

Metaclasses can also implement __prepare__ to swap the class namespace dict for something else (an OrderedDict subclass, a tracking dict). This is rarely needed and __init_subclass__ covers the common cases — mention only.

Modern features have largely replaced metaclasses:

Old reason for a metaclass Modern replacement
Track all subclasses __init_subclass__
Inject descriptors based on annotations __init_subclass__ + __set_name__
Customize class namespace rarely needed; sometimes class decorators
Generate __init__/__repr__/__eq__ @dataclass

The rule, in plain English: if you think you need a metaclass, you probably don’t. Walk down the ladder of complexity until something fits:

  1. @dataclass — generates __init__/__repr__/__eq__. The right answer 80% of the time.
  2. A class decorator@register, @checked, @final. Runs once, no inheritance contract.
  3. __init_subclass__ — fires on every subclass without writing a metaclass.
  4. __set_name__ on a descriptor — when the customization is per-attribute, not per-class.
  5. A metaclass — last resort. Reserved for framework authors.

Reach for the simplest tool that solves the problem.

TipWhy this matters — and the final insight of the whole book

Python is a language with a consistent, uniform data model. Functions are objects. Classes are objects. Metaclasses are classes. Everything is subject to the same rules: attribute access, the descriptor protocol, the iterator protocol. The “magic” is not magic — it’s a carefully designed system where the same machinery applies at every level. When you see a built-in like len() or for or with, you are seeing the data model at work. Implement the right special methods and your code becomes part of the language itself.

36.6 Build: a self-registering Plugin system with class kwargs and dispatch

The canonical use-case for __init_subclass__: auto-register every subclass into a central registry, with a per-subclass keyword argument to control the registration key. We’ll build the base, the dispatch, and finally an alternative metaclass version that catches subclasses without a shared base — to make the choice between the two tools concrete.

Step 1: a Plugin base whose __init_subclass__ auto-registers. Class-keyword arguments are the modern way to parametrise subclass declarations — they show up as keyword arguments to __init_subclass__:

class Plugin:
    registry: dict[str, type["Plugin"]] = {}

    def __init_subclass__(cls, *, name: str | None = None, **kwargs):
        super().__init_subclass__(**kwargs)
        key = name or cls.__name__.lower()
        Plugin.registry[key] = cls

    def run(self, **kwargs):
        raise NotImplementedError(f"{type(self).__name__} must implement run()")

class Greeter(Plugin, name="hello"):
    def run(self, who: str = "world") -> str:
        return f"Hello, {who}!"

class Echoer(Plugin, name="echo"):
    def run(self, text: str) -> str:
        return text

class ShoutPlugin(Plugin):                            # default key from cls.__name__
    def run(self, text: str) -> str:
        return text.upper() + "!"

list(Plugin.registry)
['hello', 'echo', 'shoutplugin']

class Greeter(Plugin, name="hello"): passes name="hello" as a class keyword. Python forwards it to __init_subclass__ along with whatever other keywords were given. The or cls.__name__.lower() fallback handles the no-keyword case (ShoutPlugin"shoutplugin"). super().__init_subclass__(**kwargs) is the discipline — forward unknown keywords up the chain so you don’t break a deeper hook. After the cell, Plugin.registry contains all three classes — no decorator on each one, no manual register() calls.

Step 2: dispatch by name with error handling. A small run(name, **kwargs) wrapper that looks up the class, instantiates it, and forwards the keyword arguments to its run method:

def run(name: str, **kwargs):
    if name not in Plugin.registry:
        raise KeyError(
            f"unknown plugin {name!r}; available: {sorted(Plugin.registry)}"
        )
    plugin = Plugin.registry[name]()
    return plugin.run(**kwargs)

[run("hello"), run("hello", who="Alice"), run("echo", text="ping"),
 run("shoutplugin", text="ok")]
['Hello, world!', 'Hello, Alice!', 'ping', 'OK!']
run("missing")
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[13], line 1
----> 1 run("missing")

Cell In[12], line 3, in run(name, **kwargs)
      1 def run(name: str, **kwargs):
      2     if name not in Plugin.registry:
----> 3         raise KeyError(
      4             f"unknown plugin {name!r}; available: {sorted(Plugin.registry)}"
      5         )
      6     plugin = Plugin.registry[name]()

KeyError: "unknown plugin 'missing'; available: ['echo', 'hello', 'shoutplugin']"

Plugin.registry[name]() looks up the class, calls it (instantiation), and the resulting instance’s .run(**kwargs) does the work. The error path includes the available names — small touch, big payoff for usability. With the __init_subclass__ registration in place, adding a new plugin is one class declaration with no other glue.

Step 3: a metaclass alternative for plugins that can’t share a base. Plugin requires every plugin to inherit from it. If you’re managing classes from third-party libraries that already have their own bases, a metaclass works through the metaclass= keyword without needing the base relationship:

class PluginMeta(type):
    registry: dict[str, type] = {}

    def __new__(mcs, name, bases, namespace, *, name_=None, **kwargs):
        cls = super().__new__(mcs, name, bases, namespace, **kwargs)
        cls._name = name_ or name.lower()
        PluginMeta.registry[cls._name] = cls
        return cls

    def __init__(cls, name, bases, namespace, *, name_=None, **kwargs):
        super().__init__(name, bases, namespace, **kwargs)

class FastDict(dict, metaclass=PluginMeta, name_="fast"):
    def run(self):
        return "I am a dict-based plugin"

class FastList(list, metaclass=PluginMeta, name_="bulk"):
    def run(self):
        return "I am a list-based plugin"

[type(FastDict).__name__, list(PluginMeta.registry)]
['PluginMeta', ['fast', 'bulk']]

PluginMeta(type) is a metaclass — its instances are classes. __new__ runs at class-creation time and registers the new class. The two plugins inherit from dict and list respectively — no shared base; the only thing they have in common is metaclass=PluginMeta. The class keyword name_= (note the trailing underscore — name is also __new__’s positional arg, so we rename to avoid collision) flows to the metaclass call. type(FastDict) is PluginMeta, confirming the metaclass relationship.

Notice the trade-off: the metaclass version reaches further (any base) but pays in complexity (extra __new__/__init__ boilerplate, the keyword-name collision quirk). For everything that can share a base, __init_subclass__ from Step 1 is cleaner. The metaclass earns its place only when the shared-base requirement is the binding constraint.

The build closes the book: __init_subclass__ for the common registration case (Steps 1–2), and a metaclass for the “no shared base” case (Step 3). Both implement the same conceptual operation — register every new class — but at different layers of the data model. The deeper tool waits for the deeper need.

36.7 Exercises

  1. Build a class with type. Use type("Point", (object,), {...}) to create a Point class with x, y attributes and an __init__. Compare with the equivalent class statement.

  2. __init_subclass__ for registration. Extend the Pluggable example so each subclass can pass a name="..." keyword in its class line, and the parent registers it under that name in a dict.

  3. Walk the chain. Print Movie.__mro__ for the example above. Where does Checked appear? Where does object?

  4. Class decorator alternative. Re-implement the Checked/__init_subclass__ example as a class decorator (@checked). When is the decorator clearer?

  5. A small metaclass. Write a Counted(type) metaclass that tracks how many instances of each class have been created. Apply it to two unrelated classes and verify each has its own count.

36.8 Summary

type is a class factory; classes are objects; __init_subclass__ and __set_name__ cover the common framework-building cases without metaclasses. The deepest tool — a custom metaclass — exists for the rare case when nothing else reaches deep enough.

That closes the book. The single thread running through all twenty-four chapters is the data model: the contracts that built-in syntax delegates to, and the dunder methods you implement to participate. Everything else — sequences, dicts, classes, decorators, descriptors, async — is variations on that one theme. The references in Chapter 18 remind us that variables are labels; the data model reminds us that every object is a participant in the same protocol-driven language.

Read PEPs. Read the standard library. Write small classes. The data model rewards the time you put in.