Skip to content
Codeloom
Python

Python Metaclasses Explained: Understanding the Class of a Class

Demystify Python metaclasses with practical examples. Learn how classes are created, when to use metaclasses, and real-world patterns.

·5 min read · By Codeloom
Advanced 14 min read

What you'll learn

  • What metaclasses are and how Python uses them
  • How class creation works under the hood
  • Building custom metaclasses for validation and registration
  • When to use metaclasses vs simpler alternatives

Prerequisites

  • Strong Python OOP knowledge
  • Understanding of class inheritance
  • Familiarity with dunder methods

The Big Idea

In Python, everything is an object — including classes. If a class is an object, then something must create it. That something is a metaclass. By default, the metaclass of every class is type. A metaclass is, quite literally, the class of a class.

class Dog:
    pass

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

Dog is an instance of type. When you write class Dog: pass, Python calls type('Dog', (object,), {}) behind the scenes to create the Dog class object.

How Classes Are Actually Created

When Python encounters a class statement, it goes through these steps:

  1. The class body is executed as a code block, collecting attributes into a dictionary.
  2. Python determines the metaclass (default is type).
  3. The metaclass is called with three arguments: name, bases, and the namespace dictionary.

You can replicate class creation manually:

# These two are equivalent:

# Standard syntax
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

# Manual creation with type()
def init(self, x, y):
    self.x = x
    self.y = y

def repr_(self):
    return f"Point({self.x}, {self.y})"

Point = type('Point', (object,), {'__init__': init, '__repr__': repr_})

Both produce identical class objects.

Writing Your First Metaclass

A metaclass is a class that inherits from type and overrides __new__ or __init__:

class ValidatedMeta(type):
    """Metaclass that enforces all methods have docstrings."""

    def __new__(mcs, name, bases, namespace):
        for attr_name, attr_value in namespace.items():
            if callable(attr_value) and not attr_name.startswith('_'):
                if not attr_value.__doc__:
                    raise TypeError(
                        f"Method '{attr_name}' in class '{name}' "
                        f"must have a docstring"
                    )
        return super().__new__(mcs, name, bases, namespace)

class MyService(metaclass=ValidatedMeta):
    def process(self):
        """Process the data."""
        pass

    def validate(self):
        """Validate inputs."""
        pass

# This would raise TypeError:
# class BadService(metaclass=ValidatedMeta):
#     def handle(self):  # No docstring!
#         pass

The __new__ method in a metaclass is called when the class is being created, not when instances of the class are created. This lets you inspect, modify, or reject class definitions at creation time.

Practical Pattern: Automatic Registration

One of the most useful metaclass patterns is automatic registration of subclasses:

class PluginMeta(type):
    """Metaclass that registers all subclasses in a registry."""
    registry = {}

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        # Don't register the base class itself
        if bases:
            plugin_name = namespace.get('name', name.lower())
            mcs.registry[plugin_name] = cls
        return cls

class Plugin(metaclass=PluginMeta):
    """Base class for all plugins."""
    name = None

    def execute(self):
        raise NotImplementedError

class CSVExporter(Plugin):
    name = "csv"

    def execute(self):
        print("Exporting to CSV")

class JSONExporter(Plugin):
    name = "json"

    def execute(self):
        print("Exporting to JSON")

# All plugins are registered automatically
print(PluginMeta.registry)
# {'csv': <class 'CSVExporter'>, 'json': <class 'JSONExporter'>}

def get_exporter(name):
    return PluginMeta.registry[name]()

Every time a new Plugin subclass is defined anywhere in the codebase, it is automatically registered. No manual registration step is needed.

Practical Pattern: Singleton via Metaclass

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 Database(metaclass=SingletonMeta):
    def __init__(self):
        self.connection = "connected"
        print("Database initialized")

db1 = Database()  # Prints "Database initialized"
db2 = Database()  # No print -- returns existing instance
print(db1 is db2)  # True

Here we override __call__ instead of __new__. The metaclass __call__ is invoked when you call the class (i.e., when creating instances), while __new__ is invoked when creating the class itself.

init_subclass: The Simpler Alternative

Python 3.6 introduced __init_subclass__, which handles many cases that previously required metaclasses:

class Plugin:
    registry = {}

    def __init_subclass__(cls, name=None, **kwargs):
        super().__init_subclass__(**kwargs)
        plugin_name = name or cls.__name__.lower()
        Plugin.registry[plugin_name] = cls

class CSVExporter(Plugin, name="csv"):
    pass

class JSONExporter(Plugin, name="json"):
    pass

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

This achieves the same registration pattern without a metaclass. Prefer __init_subclass__ when it meets your needs.

When Should You Actually Use Metaclasses?

Metaclasses are powerful but rarely necessary. Use them when:

  • You need to modify the class creation process itself (not just subclass behavior).
  • You need to enforce constraints across all classes using the metaclass.
  • You are building a framework where users define classes that must follow specific rules (like Django models or SQLAlchemy).

Do not use metaclasses when:

  • __init_subclass__ can do the job.
  • Class decorators can achieve the same result.
  • Simple inheritance solves the problem.
# Often a class decorator is simpler than a metaclass
def add_logging(cls):
    original_init = cls.__init__

    def new_init(self, *args, **kwargs):
        print(f"Creating {cls.__name__}")
        original_init(self, *args, **kwargs)

    cls.__init__ = new_init
    return cls

@add_logging
class MyClass:
    def __init__(self, value):
        self.value = value

The Metaclass Resolution Order

When a class has multiple bases with different metaclasses, Python must find a single metaclass that is a subclass of all of them. If no such metaclass exists, you get a TypeError:

class MetaA(type): pass
class MetaB(type): pass

class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass

# This fails:
# class C(A, B): pass
# TypeError: metaclass conflict

# Fix by creating a combined metaclass:
class MetaC(MetaA, MetaB): pass
class C(A, B, metaclass=MetaC): pass

Wrapping Up

Metaclasses are the mechanism behind Python’s class creation. They power frameworks like Django and SQLAlchemy, enable automatic registration patterns, and enforce coding standards at the class level. However, with __init_subclass__ and class decorators available, you should reach for metaclasses only when simpler tools fall short. Understanding them deeply, even if you rarely write them, will make you a better Python developer.