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.
__call__ in Metaclasses
__call__
on a metaclass fires when you
instantiate the class itself
— not when the class is being created. Let's have a look into the full picture:
class MetaA(type):
def __new__(mcls, name, bases, namespace):
print("1) MetaA.__new__ — creating the CLASS")
return super().__new__(mcls, name, bases, namespace)
def __init__(cls, name, bases, namespace):
print("2) MetaA.__init__ — initializing the CLASS")
super().__init__(name, bases, namespace)
def __call__(cls, *args, **kwargs):
print("MetaA.__call__ starts")
instance = super().__call__(*args, **kwargs) # this is what triggers A.__new__ and A.__init__
print("MetaA.__call__ ends")
return instance
class A(metaclass=MetaA):
def __new__(cls, *args, **kwargs):
print(" A.__new__")
return super().__new__(cls, *args, **kwargs)
def __init__(self, ):
print(" A.__init__")
print("--- defining A above ---")
a = A() # <-- THIS triggers __call__
print(a)
##################################
#Output
##################################
"""
1) MetaA.__new__ — creating the CLASS
2) MetaA.__init__ — initializing the CLASS
--- defining A above ---
MetaA.__call__ starts
A.__new__
A.__init__
MetaA.__call__ ends
<__main__.A object at 0x0000014447EC4D00>
"""
Key insight:
MetaA.__call__
is what makes
A()
work at all! Normally
type.__call__
does the job of calling
A.__new__
then
A.__init__
for you. If you override
__call__
in your metaclass, you can
intercept instance creation
— e.g., for singletons, caching, or logging every instantiation.
One __call__ syntax, two Timeline
Each regular class has __call__ as well. Same syntax, completely different machinery depending on what the object is . To understand it better let's separate two things that look similar but at completely different moments:
a = A() # triggers MetaA.__call__ — because A is an instance of MetaA
a() # triggers A.__call__ — because a is an instance of A
The two timelines
When you write
class A(metaclass=MetaA):
, Python runs through Timeline 1 — once, never again:
MetaA.__new__ → MetaA.__init__
This builds the class object A itself. Think of it as constructing the blueprint. By the time this finishes, A exists as an object in memory and MetaA.__init__ is done forever.
Every time you later write
A()
, Python runs Timeline 2:
MetaA.__call__ → A.__new__ → A.__init__
These two timelines are not adjacent steps in one sequence. Timeline 1 happens at class definition time. Timeline 2 happens at instantiation time, potentially millions of times, long after Timeline 1 is ancient history.
Normally
type.__call__
handles Timeline 2 for you — it calls
A.__new__
to allocate the instance, then
A.__init__
to initialise it. You never think about it.
we know metaclass has access what are in the class body, including class vars, but not object variables, but it also has access to obj methods, why? this gets at something fundamental about what's actually "in" a class. The key insight:
methods are not object-level things, they're class-level things.
When Python executes the class body to build
namespace
, it runs:
class A:
def __init__(self):
self.x = 10 # instance variable — lives on the OBJECT
def greet(self): # method — lives on the CLASS
print("hi")
def
__init__
(
self
)
:
.
.
.
def
greet
(
self
)
:
...
These are just
function definitions,
at this point,
def greet(self):
is no different from
x = 10
or
name = "Alice"
. It's just another name bound in the class body's namespace. Python doesn't know or care yet that
greet
is "a method", it's simply a function object sitting in the dict, same as any other variable.
o where does
self.x
go, and why can't the metaclass see it?
self.x = 10
only happens
inside
__init__
, when an instance is created
— i.e., during
A()
, long after the class itself (and its metaclass logic) has already finished running. The metaclass only ever sees the
class body
, executed once. Instance attributes like
self.x
don't exist yet — they're created later, per-instance, at instantiation time, which is
outside the metaclass's view entirely
.
The deeper reason methods "belong" to the class:
Methods are
shared
— every instance of
A
uses the
same
greet
function object. That's literally why it lives on the class (so it's defined once, not duplicated per instance). Instance variables, by contrast, are
per-object data
— each instance needs its own
x
, so they live on the instance's own
__dict__
, created fresh during
__init__
.
Regular Class__new__
vs Metaclass
__call__
A common question when learning metaclasses is difference between a class __new__ and metaclass __call__ while instantiating. we explore this question through Singleton in Python; If you can implement a Singleton using a class's own
__new__
, why would you ever reach for a metaclass
__call__
instead? Both
can
solve the same problem, but they operate at different levels, and that difference matters once your requirements grow.
Singleton via
__new__
class DatabaseConnection:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, connection_string):
print('initialising ...')
self.connection_string = connection_string
db1= DatabaseConnection('connection_string_1')
db2= DatabaseConnection('connection_string_2')
print('If two instances are the same:', db1 is db2 )
print(db1.connection_string)
print(db2.connection_string)
############################
#Output
###########################
"""
initialising ...
initialising ...
If two instances are the same: True
connection_string_2
connection_string_2
"""
This works,
db1 is db2
is
True
. But there's a catch:
__init__
still runs every time
, even when
__new__
returns the cached instance. So
DatabaseConnection("postgres://otherhost")
will silently overwrite
connection_string
on the existing object. You'd need extra guard logic inside
__init__
itself to prevent re-initialization.
Singleton via Metaclass
__call__
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabaseConnection(metaclass=SingletonMeta):
def __init__(self, connection_string):
print('initialising ...')
self.connection_string = connection_string
db1= DatabaseConnection('connection_string_1')
db2= DatabaseConnection('connection_string_2')
print('If two instances are the same:', db1 is db2 )
print(db1.connection_string)
print(db2.connection_string)
############################
#Output
###########################
"""
initialising ...
If two instances are the same: True
connection_string_1
connection_string_1
"""
Here,
__call__
intercepts the
entire instantiation process
—
super().__call__()
is what triggers
__new__
and
__init__
together. If the instance already exists in
_instances
, neither
__new__
nor
__init__
run again. No guard logic needed inside the class itself — the class body stays clean.
The real distinction
__new__
on the class
|
__call__
on the metaclass
|
|
|---|---|---|
| Controls | Object creation only | The whole act of "calling" the class |
__init__
still runs?
|
Yes, every time | No — skipped entirely if cached |
| Where the logic lives | Inside the class itself | Outside, in a reusable metaclass |
| Reusable across many classes? | Must repeat in every class |
Write once, attach via
metaclass=
|
That last row is the real reason to prefer a metaclass: if you have ten classes that all need to be singletons,
__new__
forces you to copy-paste the same guard into each one. With
SingletonMeta
, you write the pattern once and apply it with
metaclass=SingletonMeta
everywhere.
General rule of thumb
-
Use
__new__/__init__on the class itself when the behavior is specific to that one class and doesn't need to be shared or enforced across many unrelated classes. -
Use a metaclass
__call__when you need to control or intercept instantiation itself — across multiple classes, transparently, without each class having to implement the logic itself.
More broadly, this mirrors the architectural reason for metaclasses at all: they exist for
class-level concerns that apply uniformly across many classes
— registries, validation rules enforced at definition time, ORMs collecting fields, or singletons. If the rule only concerns one class's instances, plain
__new__
/
__init__
is usually enough. If the rule needs to apply
as a pattern
across many classes consistently, a metaclass is the right tool.
__init_subclass__
The Lightweight Alternative to Metaclasses
Metaclasses are powerful, but for many of the common use cases — registries, validation rules, enforcing required attributes, Python gives you a much lighter tool:
__init_subclass__
. It solves the same class of problems without the conceptual overhead of writing a custom metaclass.
The core idea
__init_subclass__
is a hook that fires automatically
every time a class is subclassed
. You define it once on a base class, and Python calls it for you on every subclass, no
metaclass=
keyword, no
type
inheritance, no separate metaclass definition at all.
class Plugin:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
print(f"{cls.__name__} was just created as a subclass of Plugin")
class JSONPlugin(Plugin): pass
class XMLPlugin(Plugin): pass
# Output:
# JSONPlugin was just created as a subclass of Plugin
# XMLPlugin was just created as a subclass of Plugin
Notice:
Plugin
itself does
not
trigger
__init_subclass__
, only its subclasses do. This is actually convenient, since it mirrors the common pattern of "skip the base class" that you'd otherwise have to write manually inside a metaclass (
if name != 'BaseClass':
).
Registry pattern, without a metaclass
This is the use case where
__init_subclass__
shines the most, it directly replaces the registry-style metaclass pattern:
class Plugin:
_registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Plugin._registry[cls.__name__] = cls
class JSONPlugin(Plugin):
def serialize(self, data):
return f"json:{data}"
class XMLPlugin(Plugin):
def serialize(self, data):
return f"xml:{data}"
print(Plugin._registry)
# {'JSONPlugin': <class 'JSONPlugin'>, 'XMLPlugin': <class 'XMLPlugin'>}
Compare this to the metaclass version from earlier articles, same outcome, but no
Registry(type)
metaclass, no
mcls.__new__
, no extra layer of indirection. Just a method on the base class itself.
Passing custom arguments
One underrated feature:
__init_subclass__
can accept
keyword arguments
passed directly in the class definition line, something a metaclass needs extra plumbing for.
class Plugin:
_registry = {}
def __init_subclass__(cls, plugin_name=None, **kwargs):
super().__init_subclass__(**kwargs)
name = plugin_name or cls.__name__.lower()
Plugin._registry[name] = cls
class JSONPlugin(Plugin, plugin_name="json"):
pass
print(Plugin._registry) # {'json': <class 'JSONPlugin'>}
This reads naturally,
plugin_name="json"
sits right where you'd expect it, in the class header.
Enforcing rules at definition time
__init_subclass__
is just as capable as a metaclass
__new__
for validation:
class ValidatedModel:
_required_attrs = ()
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
for attr in cls._required_attrs:
if not hasattr(cls, attr):
raise TypeError(f"{cls.__name__} must define '{attr}'")
class APIEndpoint(ValidatedModel):
_required_attrs = ("path", "method")
path = "/users"
method = "GET" # fine
# class BrokenEndpoint(ValidatedModel):
# _required_attrs = ("path", "method")
# path = "/users"
# # missing 'method' → raises TypeError immediately
Where it stops being enough
__init_subclass__
only fires at
class creation time,
the same moment as metaclass
__new__
/
__init__
. It has no equivalent of metaclass
__call__
, which means it
cannot intercept instantiation
. If your goal is something like a Singleton, controlling what happens every time
ClassName()
is called —
__init_subclass__
simply isn't the right tool; that's still metaclass
__call__
territory (or a class decorator, as covered in an earlier piece).
It also can't help if you need to control the
metaclass-of-the-metaclass
-level concerns, things like combining two incompatible metaclasses, or controlling what
kind
of class object gets built (
__new__
on the metaclass can swap out the entire
namespace
before a class is even built;
__init_subclass__
only runs once the class already fully exists).
Rule of thumb
| Need | Tool |
|---|---|
| Run code when a class is subclassed |
__init_subclass__
|
| Validate required attributes at definition time |
__init_subclass__
|
| Auto-register subclasses |
__init_subclass__
|
Intercept instantiation (
ClassName()
calls)
|
metaclass
__call__
or class decorator
|
| Modify the class body / namespace before the class exists |
metaclass
__new__
|
| Resolve conflicts between multiple incompatible metaclasses | metaclass |
The takeaway:
reach for
__init_subclass__
first. It covers most of what people think they need a metaclass for, with a fraction of the conceptual weight — no separate metaclass class, no
type
subclassing, no extra layer between
class
and
object
. Save metaclasses for the cases
__init_subclass__
genuinely can't reach: controlling instantiation itself, or manipulating the class body before the class is born.
Classes OOP Objects Python