Python

Metaclasses in Python

Advanced

Metaclasses in Python

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:

  • type is an instance of itself.

  • type is a subclass of object .

  • object is an instance of type .

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'>)

bases is used to compute __mro__ . This means that Python takes the bases tuple 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, bases is the input, and __mro__ is the output of that computation. The reason Python doesn't use bases directly for lookups is that bases contains only the immediate parent classes. When you call john.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 in namespace , 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

← All training