DocsHub
Advanced

Metaclasses

Learn what metaclasses are, how type() works, and how to create custom metaclasses in Python.

Metaclasses

This is the most advanced topic in this entire guide. Most Python developers never need to write a metaclass directly — but understanding them explains a lot of "magic" you see in popular frameworks like Django, SQLAlchemy, and Pydantic.

Take your time here. Build up slowly.


Everything in Python is an object

You already know this for normal values:

x = 42
name = "Ali"
items = [1, 2, 3]

But it is also true for classes:

class Dog:
    def bark(self):
        print("Woof!")

print(type(x))      # <class 'int'>
print(type(name))   # <class 'str'>
print(type(Dog))    # <class 'type'>

Dog is an object too. Its type is type. That means type is the class of classes — it is what creates class objects.


type() — two different jobs

type() does two completely different things depending on how you call it:

One argument — check the type:

print(type(42))          # <class 'int'>
print(type("hello"))     # <class 'str'>
print(type([1, 2, 3]))   # <class 'list'>

Three arguments — create a new class:

# type(name, bases, namespace)
Dog = type("Dog", (), {"sound": "Woof"})

print(Dog)              # <class '__main__.Dog'>
print(Dog.sound)        # Woof
print(type(Dog))        # <class 'type'>

This is exactly what Python does when it processes a class statement. Writing:

class Dog:
    sound = "Woof"

Is equivalent to:

Dog = type("Dog", (), {"sound": "Woof"})

The class keyword is syntactic sugar — type is doing the actual work.


Creating a class with type() fully

def bark(self):
    print(f"{self.name} says: Woof!")

def __init__(self, name):
    self.name = name

Dog = type(
    "Dog",                          # class name
    (object,),                      # base classes — tuple
    {                               # class namespace
        "__init__": __init__,
        "bark": bark,
        "species": "Canis familiaris"
    }
)

rex = Dog("Rex")
rex.bark()              # Rex says: Woof!
print(Dog.species)      # Canis familiaris
print(type(Dog))        # <class 'type'>

What is a metaclass?

A metaclass is a class whose instances are classes. Just like a class defines how its instances behave, a metaclass defines how its classes behave.

normal:     class  →  creates  →  instance
metaclass:  metaclass  →  creates  →  class
creates creates is the metaclass of is the class of type class Dog rex = Dog

type is the default metaclass for every class in Python. When you write class Dog:, Python uses type to build the Dog class object.

A custom metaclass lets you intercept and customize this process.


Creating a custom metaclass

Inherit from type:

class MyMeta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"Creating class: {name}")
        return super().__new__(mcs, name, bases, namespace)

Use it with metaclass=:

class Dog(metaclass=MyMeta):
    def bark(self):
        print("Woof!")

class Cat(metaclass=MyMeta):
    def meow(self):
        print("Meow!")

Output — this runs when the class is defined, not when you create instances:

Creating class: Dog
Creating class: Cat

__new__ on a metaclass receives:

  • mcs — the metaclass itself
  • name — the name of the class being created
  • bases — tuple of base classes
  • namespace — dictionary of the class body

Real example 1 — enforcing naming conventions

A metaclass that enforces all methods in a class must be lowercase:

class EnforceLowercase(type):
    def __new__(mcs, name, bases, namespace):
        for key in namespace:
            if key.startswith("_"):
                continue   # skip dunder methods
            if key != key.lower():
                raise TypeError(
                    f"Method '{key}' in class '{name}' must be lowercase. "
                    f"Use '{key.lower()}' instead."
                )
        return super().__new__(mcs, name, bases, namespace)


class GoodAPI(metaclass=EnforceLowercase):
    def get_user(self):       # fine — lowercase
        pass

    def create_order(self):   # fine — lowercase
        pass


class BadAPI(metaclass=EnforceLowercase):
    def GetUser(self):        # TypeError — not lowercase
        pass

Output:

TypeError: Method 'GetUser' in class 'BadAPI' must be lowercase. Use 'getuser' instead.

The error happens when the class is defined — before any instance is ever created.


Real example 2 — automatic class registration

A common pattern in frameworks — automatically register every subclass so you can look them up by name later:

class PluginMeta(type):
    registry = {}

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if bases:   # skip the base class itself
            mcs.registry[name] = cls
            print(f"Registered plugin: {name}")
        return cls


class Plugin(metaclass=PluginMeta):
    """Base class for all plugins."""
    def run(self):
        raise NotImplementedError


class CSVPlugin(Plugin):
    def run(self):
        print("Processing CSV data")


class JSONPlugin(Plugin):
    def run(self):
        print("Processing JSON data")


class XMLPlugin(Plugin):
    def run(self):
        print("Processing XML data")


print(PluginMeta.registry)
# {'CSVPlugin': <class 'CSVPlugin'>, 'JSONPlugin': <class 'JSONPlugin'>, 'XMLPlugin': <class 'XMLPlugin'>}

# look up and use a plugin by name
plugin_name = "JSONPlugin"
plugin = PluginMeta.registry[plugin_name]()
plugin.run()   # Processing JSON data

Output:

Registered plugin: CSVPlugin
Registered plugin: JSONPlugin
Registered plugin: XMLPlugin

Every subclass of Plugin is automatically registered the moment it is defined. No manual registration needed — the metaclass handles it.


Real example 3 — adding methods automatically

A metaclass that automatically adds a to_dict() method to every class:

class AutoDict(type):
    def __new__(mcs, name, bases, namespace):
        def to_dict(self):
            return {
                key: value
                for key, value in self.__dict__.items()
                if not key.startswith("_")
            }

        namespace["to_dict"] = to_dict
        return super().__new__(mcs, name, bases, namespace)


class User(metaclass=AutoDict):
    def __init__(self, name, age, email):
        self.name = name
        self.age = age
        self.email = email


class Product(metaclass=AutoDict):
    def __init__(self, name, price, stock):
        self.name = name
        self.price = price
        self.stock = stock


user = User("Ali", 22, "ali@example.com")
print(user.to_dict())
# {'name': 'Ali', 'age': 22, 'email': 'ali@example.com'}

product = Product("Laptop", 999.99, 50)
print(product.to_dict())
# {'name': 'Laptop', 'price': 999.99, 'stock': 50}

Both classes got to_dict() automatically — without inheriting from anything or manually defining it.


init_subclass — a simpler alternative

Before reaching for a metaclass, check if __init_subclass__ solves your problem. It is simpler and works for most common use cases — it runs every time a class inherits from your base class:

class Plugin:
    registry = {}

    def __init_subclass__(cls, plugin_type=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if plugin_type:
            Plugin.registry[plugin_type] = cls
            print(f"Registered: {plugin_type}{cls.__name__}")


class CSVPlugin(Plugin, plugin_type="csv"):
    def run(self):
        print("Processing CSV")


class JSONPlugin(Plugin, plugin_type="json"):
    def run(self):
        print("Processing JSON")


print(Plugin.registry)
# {'csv': <class 'CSVPlugin'>, 'json': <class 'JSONPlugin'>}

Plugin.registry["csv"]().run()   # Processing CSV

Output:

Registered: csv → CSVPlugin
Registered: json → JSONPlugin

Use __init_subclass__ before writing a metaclass. It handles most subclass customization cases with far less complexity. Only reach for a metaclass when you need to customize the class creation process itself — modifying the namespace, changing bases, intercepting __new__ on the class level.


class decorators — another simpler alternative

Class decorators can also modify a class after it is created — simpler than a metaclass for many use cases:

def add_repr(cls):
    """Add a __repr__ to any class automatically."""
    def __repr__(self):
        attrs = ", ".join(
            f"{k}={v!r}"
            for k, v in self.__dict__.items()
            if not k.startswith("_")
        )
        return f"{cls.__name__}({attrs})"

    cls.__repr__ = __repr__
    return cls


@add_repr
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age


@add_repr
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price


print(User("Ali", 22))          # User(name='Ali', age=22)
print(Product("Laptop", 999))   # Product(name='Laptop', price=999)

When to use each tool

ToolUse when
Class decoratorModify or add to a class after it is defined
__init_subclass__Customize behavior when subclassing
MetaclassIntercept class creation itself — modify namespace, enforce rules at definition time
dataclassesAuto-generate __init__, __repr__, __eq__ — covers most data class needs

A well-known Python saying: if you think you need a metaclass, you probably do not. They are powerful but complex. Most problems that feel like they need a metaclass can be solved more cleanly with __init_subclass__, class decorators, or dataclasses. Use metaclasses when you are building a framework and genuinely need to control the class creation process.


Summary

ConceptMeaning
type(x)Get the type of x
type(name, bases, namespace)Create a class dynamically
MetaclassThe class that creates a class — default is type
Custom metaclassInherit from type, override __new__
metaclass=MyMetaUse a custom metaclass for a class
__init_subclass__Simpler alternative for subclass customization
Class decoratorSimpler alternative for post-creation modification

On this page