Metaclasses, Class Creation & init_subclass
In Python, Classes are Instances of Metaclasses. Just as an object instance is created by its class, a class object is created by its metaclass (type by default). Metaclasses control class instantiation, attribute validation, and registration at module load time.
This chapter details the 4-step CPython class creation pipeline, type.__new__ vs type.__init__, custom metaclass implementation, and the modern PEP 487 __init_subclass__ hook.
1. The CPython 4-Step Class Creation Pipeline
When Python executes a class MyClass(Base): block:
CPython Class Creation Pipeline:
[ 1. Evaluate Class Body Code ]
(Executes top-level statements inside a new dictionary scope)
|
v
[ 2. Determine Metaclass (metaclass=Meta) ]
(Defaults to 'type' if no explicit metaclass is specified)
|
v
[ 3. Invoke Meta.__new__(meta, name, bases, namespace) ]
(Allocates the new PyTypeObject struct in C memory)
|
v
[ 4. Invoke Meta.__init__(cls, name, bases, namespace) ]
(Initializes the class object namespace dictionary)2. Metaclass Implementation Mechanics (type)
To write a custom metaclass, inherit from type and override __new__():
class ModelMetaclass(type):
def __new__(cls, name, bases, namespace):
# Intercept class creation: Ensure every ORM class defines a primary key!
if name != "BaseModel":
if "id" not in namespace:
raise TypeError(f"Class '{name}' must define a primary key 'id'")
# Transform namespace: Auto-inject table name if absent
if "__tablename__" not in namespace:
namespace["__tablename__"] = name.lower() + "s"
# Delegate allocation to type.__new__()
return super().__new__(cls, name, bases, namespace)
class BaseModel(metaclass=ModelMetaclass):
pass
class User(BaseModel):
id: int # VALID: Passes primary key check; auto-injects __tablename__ = 'users'3. Modern Lightweight Alternative: __init_subclass__ (PEP 487)
Writing full metaclasses adds complexity and risks metaclass conflict errors when inheriting across framework boundaries.
Introduced in PEP 487 (Python 3.6+), __init_subclass__ provides a simple hook inside standard classes to intercept and customize subclass creation without custom metaclasses:
class RegistryBase:
_registry = {}
# Called automatically whenever a subclass is defined!
def __init_subclass__(cls, plugin_name: str | None = None, **kwargs):
super().__init_subclass__(**kwargs)
if plugin_name:
cls._registry[plugin_name] = cls
# Subclasses register themselves cleanly via class parameters!
class JSONExporter(RegistryBase, plugin_name="json"):
pass
class CSVExporter(RegistryBase, plugin_name="csv"):
pass
print(RegistryBase._registry)
# {'json': <class '__main__.JSONExporter'>, 'csv': <class '__main__.CSVExporter'>}4. Metaclass Conflicts & Rule of Thumb
- Metaclass Conflict: Occurs when a class inherits from multiple parent classes that use different, non-derived metaclasses (
TypeError: metaclass conflict). - Rule of Thumb (Tim Peters): “Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t.” Prefer
__init_subclass__or Class Decorators for 95% of class registration and attribute validation needs.