Practical guide · Python

Python Metaclasses Explained with Real-World Examples

Understand how Python creates classes, how custom metaclasses can validate or register them, and when simpler tools are a better fit.

Python classes are objects too. Just as a class describes how its instances behave, a metaclass describes how a class is constructed. Metaclasses are useful when a framework needs to consistently validate or modify many class definitions, but they add complexity and are rarely needed for ordinary application logic.

What is a metaclass?

For a typical class, the relationship is:

class Employee:
    pass

employee = Employee()

print(type(employee))
# <class '__main__.Employee'>
print(type(Employee))
# <class 'type'>

The phrase “a class of a class” is a useful shorthand: type is the class that creates ordinary classes, and Employee creates employee instances.

Create a class with type

The three-argument form of type creates a class dynamically. It takes the class name, a tuple of base classes, and a dictionary of attributes:

Employee = type(
    "Employee",
    (),
    {"company": "Example Corp"},
)

print(Employee.company)
# Example Corp

This is similar in spirit to a simple class statement, but class statements are more readable for most code. Custom metaclasses let you customize the class-construction process itself.

Write a custom metaclass

A custom metaclass usually subclasses type. Its __new__ method receives the metaclass, the new class's name, its base classes, and the namespace containing its attributes. It can inspect or adjust that namespace before delegating class creation to type:

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


class Employee(metaclass=LoggingMeta):
    pass

# Creating class: Employee

The hook is named __new__ with two leading and two trailing underscores. A method named new is just an ordinary method and will not intercept class creation.

Add a method to classes

A metaclass can add a default method to every class that uses it. setdefault preserves a class's own implementation when it already defines that method:

class GreetingMeta(type):
    def __new__(mcls, name, bases, namespace):
        def greet(self):
            return "Hello"

        namespace.setdefault("greet", greet)
        return super().__new__(mcls, name, bases, namespace)


class Employee(metaclass=GreetingMeta):
    pass

print(Employee().greet())
# Hello

Transform class attributes

A metaclass can rewrite the namespace before class creation. This example uppercases non-dunder attribute names while leaving Python's special attributes unchanged:

class UpperAttrsMeta(type):
    def __new__(mcls, name, bases, namespace):
        updated = {
            key if key.startswith("__") else key.upper(): value
            for key, value in namespace.items()
        }
        return super().__new__(mcls, name, bases, updated)


class Person(metaclass=UpperAttrsMeta):
    city = "Hyderabad"

print(Person.CITY)
# Hyderabad

This demonstrates namespace rewriting, but renaming attributes can surprise users and break expected APIs. Apply transformations only when the convention is explicit and consistently enforced.

Validate class definitions

Frameworks can use a metaclass to reject an invalid class as it is being defined. This example requires each concrete model to declare its own table_name:

class ModelMeta(type):
    def __new__(mcls, name, bases, namespace):
        if name != "BaseModel" and "table_name" not in namespace:
            raise TypeError(f"{name} must define table_name")
        return super().__new__(mcls, name, bases, namespace)


class BaseModel(metaclass=ModelMeta):
    pass


class User(BaseModel):
    table_name = "users"


class Product(BaseModel):
    pass  # Raises TypeError during class definition

This check deliberately requires an attribute in each subclass's own namespace. A real framework should document whether inherited values count and whether abstract base classes are exempt.

Register plugin classes automatically

Metaclasses can collect related classes in a registry as they are defined. A base class can opt out of registration while concrete plugins are recorded under explicit names:

class PluginMeta(type):
    registry = {}

    def __new__(mcls, name, bases, namespace):
        plugin_class = super().__new__(mcls, name, bases, namespace)
        if not namespace.get("_abstract", False):
            plugin_name = namespace.get("plugin_name", name)
            mcls.registry[plugin_name] = plugin_class
        return plugin_class


class PluginBase(metaclass=PluginMeta):
    _abstract = True


class JsonPlugin(PluginBase):
    plugin_name = "json"


class CsvPlugin(PluginBase):
    plugin_name = "csv"


print(sorted(PluginMeta.registry))
# ['csv', 'json']

In production, define how duplicate names, abstract classes, imports, and plugin initialization are handled. For many projects, an explicit registration function is easier to understand and debug.

Metaclass or class decorator?

A class decorator can modify a class after its creation, which is often enough for a one-off addition:

def add_greeting(cls):
    def greet(self):
        return "Hello"

    cls.greet = greet
    return cls


@add_greeting
class Employee:
    pass

When should you use a metaclass?

Metaclasses can be appropriate for ORMs, validation frameworks, plugin architectures, dependency-injection systems, or other framework-level features that must inspect many class definitions consistently. They can enforce conventions, add methods, or populate registries automatically.

Avoid them for ordinary business rules, simple APIs, small scripts, or data processing that a function, decorator, class factory, or __init_subclass__ can handle. Metaclasses can interact with inheritance and other metaclasses, so a custom metaclass may also create compatibility conflicts. Prefer the least powerful mechanism that clearly solves the problem.

Remember: type creates ordinary classes; a custom metaclass customizes that creation process. Reach for one when a framework needs consistent class-level behavior, not just because it is possible.