Metaclasses are one of Python’s most powerful and most misunderstood features. The common explanation — ‘a metaclass is the class of a class’ — is technically correct but practically useless for understanding what actually happens when Python processes a class statement. This post does a real python metaclass deep dive by reading CPython’s type.__new__ implementation directly, tracing every significant action from class body execution to the final type object.
The class Statement: What Python Actually Does
When Python encounters class Foo(Bar, metaclass=Meta): …, the compiler generates a LOAD_BUILD_CLASS opcode. At runtime, this calls the builtins.__build_class__ function (Python/bltinmodule.c). __build_class__ does three things: it executes the class body as a function to produce a namespace dict, it determines the metaclass, and it calls the metaclass with (name, bases, namespace) to construct the class object.
Metaclass determination follows a specific algorithm: if metaclass= is explicit, use it; otherwise, use the most-derived metaclass among the base classes; if no bases, default to type.
type.__new__: The Constructor
For classes whose metaclass is type (the default), type.__new__ is called. This is implemented in type_new() in Objects/typeobject.c — one of the longest functions in CPython. Here is what it does, in order:
It validates the bases tuple and resolves the MRO (Method Resolution Order) using the C3 linearization algorithm (mro_internal() → calculate_mro()). The MRO is stored as tp_mro on the new type object. Getting the MRO wrong would break attribute lookup for all instances.
It allocates the new type object with _PyObject_GC_New(). The size of the allocation depends on whether the type has __slots__, which affects tp_basicsize.
It sets dozens of tp_* slots on the new type: tp_name, tp_bases, tp_dict (the class’s __dict__), tp_base (the primary base), tp_mro.
__slots__: What It Really Does
When __slots__ is present in the class dict, type_new() strips those names from the class dict and creates descriptor objects (PyMemberDef-backed) for each slot. These descriptors are added to the class dict. The type’s tp_basicsize is increased to accommodate the slots as fixed-offset C struct members rather than dict entries. This is why __slots__ classes use less memory per instance — instance dict allocation is suppressed unless __dict__ is explicitly in __slots__.
class Point: __slots__ = (‘x’, ‘y’) def __init__(self, x, y): self.x = x self.y = y # No instance __dict__; x and y are C-level struct members
→ Related: How Python coroutines actually work — coroutines are also built on type machinery (Blog 02)
__init_subclass__ and __set_name__
After the type is constructed but before type.__new__ returns, two hooks are called. For each descriptor in the new class’s dict that implements __set_name__, CPython calls descriptor.__set_name__(owner, name). This is how Python’s dataclasses and descriptor-based ORMs can introspect their field names at class creation time without any metaclass.
Then, the parent class’s __init_subclass__ is called with the new class as an argument. This is a cooperative hook added in Python 3.6 that lets base classes react to subclassing without requiring a metaclass.
type.__init__ vs type.__new__
Python calls both __new__ and __init__ when creating a class. For type, __init__ (type_init()) simply validates that the arguments are consistent — it does not do substantial work. The real construction happens in __new__. This is why metaclass __new__ methods receive the final class dict and can modify it, while __init__ receives the already-constructed class object.
Custom Metaclasses: __prepare__ and namespace
Before the class body executes, Python calls metaclass.__prepare__(name, bases, **kwargs) if it exists. __prepare__ returns the namespace object — usually a plain dict, but it can be an OrderedDict or custom mapping. This is how tools like Python’s enum.EnumMeta control the order of class body attributes or prevent duplicate names.
ABCMeta (Lib/abc.py) is a good example: its __new__ registers the ABC machinery and modifies __abstractmethods__ on the class, all without touching the class body.
The MRO Algorithm
C3 linearization (mro_internal() → mro_implementation()) computes the MRO as follows: the class itself comes first, then merge the MROs of the bases with the bases list itself using a left-to-right, depth-first merge that respects local precedence order. If a consistent linearization cannot be found (diamond problem with conflicting orders), Python raises TypeError.
Reading calculate_mro() in typeobject.c makes it clear why some multiple-inheritance patterns fail — the algorithm is strict about monotonicity of base class ordering.
Conclusion
A Python metaclass deep dive reveals that class creation is a multi-stage pipeline: namespace preparation, class body execution, metaclass selection, type.__new__ for allocation and MRO computation, descriptor initialization, and subclass hooks. Reading type_new() in typeobject.c is the definitive reference. Once you have read it, metaclass behavior stops being mysterious and becomes predictable C code.


Leave a Reply