Python
Metaclasses in Python
Advanced
Metaclasses are Python’s mechanism for controlling class creation itself. While classes define how objects are created, metaclasses define how classes are created. In this exploration, we saw that
type
is the default metaclass behind all classes, and that every class is ultimately an instance of
type
. We also examined how
__new__
and
__init__
shape object creation, and how
__new__
is responsible for allocating instances before initialization occurs. Building on this, we introduced custom metaclasses, where
Meta.__new__
receives
name
,
bases
, and
namespace
to fully control class construction. Finally, we clarified how metaclass inheritance, MRO resolution, and metaclass conflicts determine how Python resolves complex class hierarchies.
Metaclasses in Python
Before exploring metaclasses in Python, we need to briefly review classes and objects. You can find more details about classes and instantiation in the post Classes in Python . Click here to learn more . As you have learned, or may already know, how instantiation works in Python, this knowledge will greatly help in understanding metaclasses.
A metaclass is the class of a class. It controls how classes themselves are created, just as classes control how objects are created.
metaclass → class → instance
Why metaclasses exist
Use them when you must:
- Enforce class-level rules
- Auto-register classes (plugins, ORMs)
- Modify class attributes/methods at definition time
- Control how a class is constructed (before any instance exists)
If we run this code:
class A:
pass
print(type(A)) # <class 'type'>
print((A.__mro__)) # (<class '__main__.A'>, <class 'object'>)
print(type.__mro__) # (<class 'type'>, <class 'object'>)
The type of class
A
is
type
, as the first print statement demonstrates. This means that
type
is Python's default metaclass; it controls the creation of all classes.
As the second print statement shows,
A.__mro__
is
(A, object)
, meaning every class implicitly inherits from
object
, which is the root of the instance hierarchy in Python.
The third print statement shows that
type.__mro__
is
(type, object)
, which tells us two things. First,
type
itself is a subclass of
object
, placing it within the same instance hierarchy. Second, and more strikingly,
type
is an instance of itself—
type(type)
returns
type
. Combined with the fact that
object
is also an instance of
type
, this creates a fundamental bootstrapping relationship at Python's core:
-
typeis an instance of itself. -
typeis a subclass ofobject. -
objectis an instance oftype.
This circular relationship between
type
and
object
is not a contradiction; it is co-defined at the C level in CPython, and everything else in Python sits cleanly beneath it.
Finally, if we want to create a custom metaclass, we inherit from
type
, just as
type
itself inherits from
object
.
class MyMeta(type):
def __new__(mcls, name, bases, namespace):
print(f"Creating class {name}")
return super().__new__(mcls, name, bases, namespace)
class User(metaclass=MyMeta):
pass
#Output
Creating class User
Above minimal custom metaclass runs when the class is defined , not when instantiated. Before explore more on metaclass, let's have some real-world examples.
1) Enforcing rules (e.g., abstract requirements)
class RequireID(type):
def __new__(mcls, name, bases, ns):
if name != "Base" and "id" not in ns:
raise TypeError("Classes must define 'id'")
return super().__new__(mcls, name, bases, ns)
class Base(metaclass=RequireID):
pass
class User(Base):
id = 1 # OK
2) Auto-registering classes (plugin pattern)
class Registry(type):
registry = {}
def __new__(mcls, name, bases, ns):
cls = super().__new__(mcls, name, bases, ns)
mcls.registry[name] = cls
return cls
class Plugin(metaclass=Registry):
pass
class A(Plugin): pass
class B(Plugin): pass
print(Registry.registry)
3) ORMs / Frameworks (real-world)
Django Models
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
# Before any Person() instance exists, Django already knows:
print(Person._meta.get_fields())
# [<django.db.models.fields.AutoField: id>,
# <django.db.models.fields.CharField: name>,
# <django.db.models.fields.IntegerField: age>]
What happens:
ModelBase
(Django's metaclass) intercepts class creation, walks the attributes, finds
Field
instances, and stores them in
Person._meta
, all before you ever call
Person()
.
Pydantic
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
# Before any Person() instance exists, Pydantic already knows:
print(Person.model_fields)
# {
# 'name': FieldInfo(annotation=str, required=True),
# 'age': FieldInfo(annotation=int, required=True)
# }
What happens:
Pydantic's metaclass (
ModelMetaclass
) reads
__annotations__
at class creation time, builds
FieldInfo
objects, and attaches validation logic — before any instance is created.
dataclasses
from dataclasses import dataclass, fields
@dataclass
class Person:
name: str
age: int
# Before any Person() instance exists, dataclasses already knows:
print(fields(Person))
# (Field(name='name', type=str, ...), Field(name='age', type=int, ...))
What happens:
The
@dataclass
decorator (not a metaclass, but same idea) inspects
__annotations__
immediately at decoration time, builds
Field
objects, and
generates and injects
__init__
,
__repr__
,
__eq__
— all before any instance exists.
Metaclasses in details
class Meta(type):
def __new__(mcls, name, bases, namespace):
print(f"mcls: {mcls}")
print(f"name: {name}")
print(f"bases: {bases}")
print(f"namespace: {namespace}")
return super().__new__(mcls, name, bases, namespace)
class Person(metaclass=Meta):
species = "human"
def greet(self): pass
class Employee(Person):
def work(self): pass
class Manager(Employee):
level = "senior"
def __init__(self):
self.level = "manager"
def manage(self):
self.level = "executive"
print("Managing...")
# Outputs
mcls: <class '__main__.Meta'>
name: Person
bases: ()
namespace: {'__module__': '__main__', '__qualname__': 'Person', 'species': 'human', 'greet': <function Person.greet at 0x0000018931B9C8B0>}
mcls: <class '__main__.Meta'>
name: Employee
bases: (<class '__main__.Person'>,)
namespace: {'__module__': '__main__', '__qualname__': 'Employee', 'work': <function Employee.work at 0x0000018931B9C940>}
mcls: <class '__main__.Meta'>
name: Manager
bases: (<class '__main__.Employee'>,)
namespace: {'__module__': '__main__', '__qualname__': 'Manager', 'level': 'senior', '__init__': <function Manager.__init__ at 0x0000018931B9C9D0>, 'manage': <function Manager.manage at 0x0000018931B9CA60>}
From the above code snippet, we can see that
Meta
fires for every class in the hierarchy.
Meta
is inherited; we only declared
metaclass=Meta
on
Person
, but
Employee
and
Manager
automatically received it as well. This is because a metaclass is inherited just like any other class attribute through the MRO. Each subclass definition triggers
Meta.__new__
separately.
As shown in the definition of
Meta
(the custom metaclass), there are four parameters in
__new__
:
mcls
,
name
,
bases
, and
namespace
.
mcls
is the metaclass itself.
name
is the class name as a string—literally the name you wrote after the
class
keyword (for example,
Person.__name__
).
bases
is a tuple of parent classes. In the above code,
bases
shows the direct parent of each class, and only the direct parent.
bases
does not represent the full ancestry; it contains only the classes explicitly listed in the class definition. If we write
class Person:
with no parent classes,
bases
is
()
, although Python still implicitly adds
object
. As the code below shows, if there is more than one direct parent,
bases
contains all of them.
class Person(Animal, Human, metaclass=Meta): ...
#Output
bases: (<class '__main__.Mammal'>, <class '__main__.Human'>)
basesis used to compute__mro__. This means that Python takes thebasestuple and runs the C3 linearization algorithm on it to determine the final method resolution order.Here's how it works:
class Person: pass class Employee(Person): pass class Manager(Employee): pass # When building Manager, bases = (Employee,) # Python runs C3 on it: # Start with Manager itself # Then walk Employee's MRO: (Employee, Person, object) # Merge them in order print(Manager.__mro__) # (Manager, Employee, Person, object)So,
basesis the input, and__mro__is the output of that computation. The reason Python doesn't usebasesdirectly for lookups is thatbasescontains only the immediate parent classes. When you calljohn.greet(), Python needs to know the full ordered chain to search through—that's what__mro__provides.The last parameter is
namespace. It contains the class body as a dictionary. This is the result of executing the class body. Python runs the class body in an isolated dictionary and then passes it here. This is a key component because it is what ORMs and frameworks inspect to find fields, annotations, and descriptors before the class is fully built. By the time__new__runs, the class body has already been executed—its contents are stored innamespace, ready to be inspected, modified, or even discarded entirely.Multiple Inheritance and Multiple Metaclasses
As shown above, there is no issue with multiple inheritance when it is straightforward. However, there is one additional thing to be aware of when combining metaclasses with multiple inheritance:
Metaclass conflict:
class MetaA(type): pass
class MetaB(type): pass
class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass
class C(A, B): pass # TypeError: metaclass conflict
# Python asks: what is the metaclass of C?
# A says: MetaA
# B says: MetaB
# Two different metaclasses → conflict → TypeError
This happens because
C
can only have one metaclass, but
A
and
B
bring in different ones. The fix is to create a combined metaclass:
class MetaC(MetaA, MetaB): pass # combines both
class C(A, B, metaclass=MetaC): pass # works fine
Why does this work?
metaclass=
answers the question:
“Who builds this class?”
, while inheriting from
type
answers:
“Are you yourself a class builder?”
MetaC
inherits from
type
→ it is a class builder → it is a metaclass.
C
is built by
MetaC
→ it is a regular class, just with a custom creator. A metaclass is any class that inherits from
type
, which makes it a class builder capable of controlling how other classes are created. When you write
metaclass=MetaC
in a class definition, you are not making that class a metaclass; you are simply telling Python which builder to use when constructing it. So
MetaC(type)
is a metaclass because it inherits from
type
and gains the ability to build classes, while
class C(metaclass=MetaC)
is just a regular class that was built by
MetaC
—the
metaclass=
parameter is about who creates you, not what you are.
So, in summary, there are two things to be careful about with multiple inheritance:
-
MRO conflict: the ordering of bases must be consistent via C3.
-
Metaclass conflict: all bases must agree on a single metaclass.
1) Real-world example
Here we’re building a metaclass-based registry system where
APIMeta
intercepts class creation and automatically registers every concrete model subclass. The registry should live on the metaclass itself, not on
APIModel
, and the registration should happen in
__init__
(since the class is fully constructed at that point, making it safer and more intuitive than
__new__
). The base class
APIModel
uses
APIMeta
as its metaclass and provides shared behavior like
to_request()
, while concrete models like
User
and
Product
inherit from it. The metaclass should ensure that
APIModel
itself is not registered, only its subclasses. Finally, the goal is that
APIModel.get_model("User")
works automatically without any manual registration logic.
class APIMeta(type):
registry = {}
def __new__(mcls, name, bases, namespace):
cls = super().__new__(mcls, name, bases, namespace) # create class first
if name != 'APIModel':
# validate AFTER class is created, checking class-level 'fields'
fields = namespace.get('fields', [])
if not fields:
raise ValueError(f"{name} must define a 'fields' list")
mcls.registry[name] = cls # store the class itself, not a tuple
return cls
def get_model(mcls, name):
return mcls.registry.get(name, f"Model '{name}' not found.")
class APIModel(metaclass=APIMeta):
fields = []
endpoint = ""
def __init__(self, **kwargs): # **kwargs is flexible — works for any model
for field in self.fields:
if field not in kwargs:
raise ValueError(f"Missing field: '{field}'")
setattr(self, field, kwargs[field])
def to_request(self):
return f"GET {self.endpoint} " + str({f: getattr(self, f) for f in self.fields})
class User(APIModel):
endpoint = "/users"
fields = ["id", "name", "email"] # declared at class level — metaclass can see this
class Product(APIModel):
endpoint = "/products"
fields = ["id", "title", "price"]
# test it
u = User(id=1, name="Alice", email="alice@example.com")
print(u.to_request())
# GET /users {'id': 1, 'name': 'Alice', 'email': 'alice@example.com'}
print(APIModel.get_model("User")) # <class '__main__.User'>
print(APIModel.get_model("Product")) # <class '__main__.Product'>
# can even instantiate from registry
ModelClass = APIModel.get_model("User")
u2 = ModelClass(id=2, name="Bob", email="bob@example.com")
print(u2.to_request())
__init__ in metaclass
Metaclass can have both
__new__
and
__init__
, same two-phase pattern — just one level up. One level up" means metaclass is to classes what classes are to objects — the same two-phase
__new__
/
__init__
pattern, just the thing being created is a class instead of an object.
class APIMeta(type):
def __new__(mcls, name, bases, namespace):
# GOOD for __new__:
# - modifying namespace BEFORE class is created
# - adding/removing class attributes early
# - controlling what kind of object gets created
namespace['_auto_added'] = True
return super().__new__(mcls, name, bases, namespace)
def __init__(cls, name, bases, namespace):
# GOOD for __init__:
# - registering the fully created class
# - post-creation validation
# - setting up relationships between classes
# - anything needing the real class object
super().__init__(name, bases, namespace)
if name != 'APIModel':
print(f"{cls} is now fully created and registered")
APIMeta.registry[name] = cls
Metaclass
__new__
shapes the class while it's being born, metaclass
__init__
configures it after it exists — exactly like regular
__new__
and
__init__
, just with classes as the "instances" instead of objects.
2) Real-world example
class Field:
def __init__(self, required=False, min_length= None):
if required and isinstance(required, bool):
self.required = required
else:
self.required = False
if min_length and min_length > 0:
self.min_length = min_length
else:
self.min_length = None
class FormMeta(type):
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
if name != 'BaseForm':
cls._fields = {k:v for k,v in namespace.items() if isinstance(v, Field)}
class BaseForm(metaclass=FormMeta):
def validate(self, data):
for field_name, field in self.__class__._fields.items():
value = data.get(field_name, None)
if field.required and value is None:
raise ValueError(f'{field_name} is required')
if field.min_length and value:
if not hasattr(value, '__len__') or len(value) < field.min_length:
raise ValueError(f"{field_name} should be a string with min length of {field.min_length}")
return True
class LoginForm(BaseForm):
username = Field(required=True)
password = Field(required=False, min_length=8)
class ProfileForm(BaseForm):
bio = Field(required=False)
age = Field(required=True)
form = LoginForm()
form.validate({"username": "alice", "password": '12345678'})
This code implements a minimal form-validation framework using a
Field
descriptor and a metaclass-based field collector.
The
Field
class acts as a simple configuration container storing validation rules such as
required
and
min_length
. Each form field is defined as a class attribute using this class.
The
FormMeta
metaclass processes form classes at creation time. It scans the class namespace, collects all
Field
instances, and stores them in a
_fields
dictionary on the class. This allows the form to keep structured metadata about its declared fields.
The
BaseForm
class uses
FormMeta
as its metaclass and provides a
validate(data: dict)
method. This method iterates over all registered fields and validates incoming input data: it ensures required fields are present and that any
min_length
constraints are satisfied for values that support length checking. If validation fails, it raises a
ValueError
.
Concrete forms like
LoginForm
and
ProfileForm
inherit from
BaseForm
and declare their fields declaratively using
Field
instances.
__new__
is responsible for creating the class object, so it receives the metaclass (
mcls
), the class name, base classes, and the namespace. It should delegate class creation to
super().__new__(mcls, name, bases, namespace)
to properly construct the class.
__init__
, on the other hand, is called after the class has already been created. It receives the newly created class (
cls
), not the metaclass. Therefore, when overriding
__init__
, you should call
super().__init__(name, bases, namespace)
(or more commonly just
super().__init__(name, bases, namespace)
depending on the parent), because at this stage you're initializing the class object, not constructing it.
In short:
__new__
uses the metaclass (
mcls
) to create the class, while
__init__
works on the already-created class (
cls
). Passing
mcls
into
super().__init__
is incorrect because
__init__
operates on the class instance, not the metaclass itself.
3) Real-world example
class Column:
def __init__(self, col_type, nullable=True):
self.type= col_type
self.nullable = nullable
class ModelMeta(type):
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
cls._columns = {k:v for k, v in namespace.items() if isinstance(v, Column)}
cls._table_name = name.lower()
class BaseModel(metaclass=ModelMeta):
def save(self, data):
for col_name, col in self._columns.items():
value = data.get(col_name, None)
if not col.nullable:
if value is None:
raise ValueError(f"{col_name} is required")
if value is not None and not isinstance(value, col.type):
raise TypeError(f"{col_name} type is {col.type} but you are saving {type(value)}")
cols = ", ".join(data.keys())
vals = ", ".join(str(v) for v in data.values())
sql_ = f"INSERT INTO {self._table_name} ({cols}) VALUES ({vals})"
print(sql_)
class User(BaseModel):
name = Column(col_type=str, nullable=False)
age = Column(col_type=int, nullable=True)
class Product(BaseModel):
title = Column(col_type=str, nullable=False)
price = Column(col_type=float, nullable=False)
u = User()
u.save({"name": "alice", "age": 30})
# INSERT INTO user (name, age) VALUES (alice, 30)
u.save({"name": "alice", "age": "thirty"})
# TypeError: age must be of type int
u.save({"age": 30})
# ValueError: name is required
This code implements a lightweight ORM-style model system using a
Column
class and a metaclass to collect schema information.
The
Column
class acts as a simple field definition, storing metadata about a database column, including its expected Python type (
col_type
) and whether it can be null (
nullable
).
The
ModelMeta
metaclass processes model classes when they are created. It scans the class namespace, collects all
Column
instances, and stores them in a
_columns
dictionary on the class. It also automatically generates a table name by converting the class name to lowercase and storing it in
_table_name
.
The
BaseModel
class uses
ModelMeta
as its metaclass and provides a
save(data: dict)
method. Before saving, the method validates the input data against the declared schema. It ensures that all non-nullable columns are present and that provided values match the expected column types. If validation fails, it raises either a
ValueError
or a
TypeError
.
After successful validation, the method generates a simple SQL
INSERT
statement using the model's table name and the supplied data.
Concrete models such as
User
and
Product
inherit from
BaseModel
and declare their schema using
Column
instances as class attributes. This allows model definitions to remain concise while automatically gaining validation and SQL generation behavior.
Classes OOP Objects Python