Skip to main content
Python · lesson 33 of 34
Python advanced Lesson 33 of 34

Python Metaclasses

Understand Python's type system, metaclasses, __new__ vs __init__, custom metaclasses, __init_subclass__, and class decorators with real executed output.

By SkillByExample · Updated

A metaclass is the class of a class: it controls how classes themselves are built. Output below is from Python 3.13.

Everything Is an Object

In Python, classes are objects too. The type of a class is its metaclass — by default, type.

class Dog:
    def bark(self):
        return "Woof"

print(type(Dog))
print(type(Dog()))
print(isinstance(Dog, type))
$ python everything_is_an_object.py
<class 'type'>
<class '__main__.Dog'>
True

type is both the metaclass of all classes and a built-in function. It’s the default metaclass.

type() as a Class Factory

You can create classes dynamically with type(name, bases, namespace):

Dog = type("Dog", (object,), {
    "sound": "Woof",
    "bark": lambda self: self.sound,
})

d = Dog()
print(d.bark())

# Equivalent to:
# class Dog:
#     sound = "Woof"
#     def bark(self):
#         return self.sound
$ python type_factory.py
Woof

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

new vs init

class Celsius:
    """An immutable temperature that refuses values below absolute zero."""

    def __new__(cls, value):
        if value < -273.15:
            raise ValueError(f"{value} degrees C is below absolute zero")
        instance = super().__new__(cls)
        return instance

    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

c = Celsius(100)
print(c.value)
try:
    Celsius(-300)
except ValueError as e:
    print(f"ValueError: {e}")

class PositiveInt(int):
    def __new__(cls, value):
        if value <= 0:
            raise ValueError("Must be positive")
        return super().__new__(cls, value)

x = PositiveInt(5)
print(x)
try:
    PositiveInt(-1)
except ValueError as e:
    print(f"ValueError: {e}")
$ python new_vs_init.py
100
ValueError: -300 degrees C is below absolute zero
5
ValueError: Must be positive

__new__ is also used for immutable types (subclassing int, str, tuple) where __init__ is too late — by the time __init__ runs, an int subclass’s value is already fixed, so validation has to happen in __new__.

Writing a Metaclass

A metaclass is a class whose instances are classes. Create one by inheriting from type:

class SingletonMeta(type):
    """Metaclass that enforces the Singleton pattern."""
    _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, url):
        self.url = url
        self.connected = False

db1 = Database("postgresql://localhost/app")
db2 = Database("different url")
print(db1 is db2)
print(db1.url)
$ python singleton_metaclass.py
True
postgresql://localhost/app

db2’s constructor call never actually runs a fresh __init__ — SingletonMeta.__call__ intercepts every Database(...) call and returns the cached instance instead, so db2.url would still be the first URL, not "different url".

Metaclass new and init

class ValidatedMeta(type):
    """Enforce that all public 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"{name}.{attr_name} is missing a docstring"
                    )
        return super().__new__(mcs, name, bases, namespace)

try:
    class MyAPI(metaclass=ValidatedMeta):
        def get_user(self, user_id):
            """Fetch a user by ID."""
            ...

        def delete_user(self, user_id):
            # Missing docstring - raises TypeError at class definition time
            ...
except TypeError as e:
    print(f"TypeError: {e}")
$ python validated_meta.py
TypeError: MyAPI.delete_user is missing a docstring

This check runs when class MyAPI(...) is defined, not when it’s instantiated — a metaclass’s __new__ executes at class-creation time, catching the mistake before anyone ever creates a MyAPI object.

init_subclass: A Simpler Alternative

For many metaclass use cases, __init_subclass__ is cleaner:

class Plugin:
    _registry = {}

    def __init_subclass__(cls, plugin_name="", **kwargs):
        super().__init_subclass__(**kwargs)
        if plugin_name:
            Plugin._registry[plugin_name] = cls

class JSONPlugin(Plugin, plugin_name="json"):
    def process(self, data): ...

class XMLPlugin(Plugin, plugin_name="xml"):
    def process(self, data): ...

print(Plugin._registry)

def get_plugin(name):
    cls = Plugin._registry.get(name)
    if cls is None:
        raise ValueError(f"Unknown plugin: {name}")
    return cls()

print(type(get_plugin("json")).__name__)
$ python init_subclass.py
{'json': <class '__main__.JSONPlugin'>, 'xml': <class '__main__.XMLPlugin'>}
JSONPlugin

Auto-registration without any metaclass boilerplate — __init_subclass__ runs automatically every time a subclass is defined, no metaclass= keyword required anywhere.

Class Decorators: Often Better Than Metaclasses

import inspect

def enforce_types(cls):
    """Add runtime type checking to all __init__ parameters."""
    original_init = cls.__init__
    sig = inspect.signature(original_init)
    hints = original_init.__annotations__   # capture from the ORIGINAL __init__, not cls.__init__

    def checked_init(self, *args, **kwargs):
        bound = sig.bind(self, *args, **kwargs)
        bound.apply_defaults()
        for param, value in list(bound.arguments.items())[1:]:  # skip self
            if param in hints and not isinstance(value, hints[param]):
                raise TypeError(
                    f"{param} must be {hints[param].__name__}, "
                    f"got {type(value).__name__}"
                )
        original_init(self, *args, **kwargs)

    cls.__init__ = checked_init
    return cls

@enforce_types
class User:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

u = User("Alice", 30)
print(u.name, u.age)
try:
    User("Alice", "30")
except TypeError as e:
    print(f"TypeError: {e}")
$ python enforce_types.py
Alice 30
TypeError: age must be int, got str

Note the hints line: it reads original_init.__annotations__, not cls.__init__.__annotations__. By the time checked_init runs, cls.__init__ has already been reassigned to checked_init itself — looking up the annotations through cls.__init__ at call time would find an empty dict and silently skip every check.

When to Use What

ToolUse When
__init_subclass__Plugin registries, validating subclasses
Class decoratorModifying or wrapping a class without inheritance
ABC / @abstractmethodDefining interfaces / abstract base classes — see the Abstraction lesson
Custom metaclassFramework-level class creation (ORMs, schema validators)

Practice

1. Add a reset() method to SingletonMeta that forgets a class’s instance, then confirm calling Database(...) after reset() creates a genuinely new instance.

Solution
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]

    def reset(cls):
        SingletonMeta._instances.pop(cls, None)

class Database(metaclass=SingletonMeta):
    def __init__(self, url):
        self.url = url

db1 = Database("url-a")
print(db1 is Database("ignored"))
Database.reset()
db2 = Database("url-b")
print(db1 is db2, db2.url)
$ python singleton_reset.py
True
False url-b

A method defined on the metaclass is callable on the class (Database.reset()), with cls bound to Database, but not on instances. After the cached instance is removed, the next call builds a new one.

2. Modify ValidatedMeta so it also rejects public methods whose name is shorter than 3 characters.

Solution
class ValidatedMeta(type):
    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"{name}.{attr_name} is missing a docstring")
                if len(attr_name) < 3:
                    raise TypeError(f"{name}.{attr_name} name too short")
        return super().__new__(mcs, name, bases, namespace)

class GoodAPI(metaclass=ValidatedMeta):
    def get_user(self, user_id):
        """Fetch a user by ID."""

print("GoodAPI created")
try:
    class ShortAPI(metaclass=ValidatedMeta):
        def go(self):
            """Too short a name."""
except TypeError as e:
    print(f"TypeError: {e}")
$ python validated_names.py
GoodAPI created
TypeError: ShortAPI.go name too short

Both checks run while the class statement executes, so a bad class never comes into existence. GoodAPI passes both rules; ShortAPI has a docstring but fails the length rule.

3. Add a third plugin, CsvPlugin, to the __init_subclass__ example without changing Plugin at all, and print the registry.

Solution
class Plugin:
    _registry = {}

    def __init_subclass__(cls, plugin_name="", **kwargs):
        super().__init_subclass__(**kwargs)
        if plugin_name:
            Plugin._registry[plugin_name] = cls

class JSONPlugin(Plugin, plugin_name="json"):
    def process(self, data): ...

class CsvPlugin(Plugin, plugin_name="csv"):
    def process(self, data): ...

print(sorted(Plugin._registry))
print(Plugin._registry["csv"].__name__)
$ python csv_plugin.py
['csv', 'json']
CsvPlugin

Defining the subclass is the registration: __init_subclass__ runs on the parent each time a subclass is created, so Plugin never needs editing to learn about new plugins.

4. Confirm, without adding any code to enforce_types, that it already rejects an unknown keyword argument such as User("Alice", 30, extra="oops").

Solution
import inspect

def enforce_types(cls):
    original_init = cls.__init__
    sig = inspect.signature(original_init)
    hints = original_init.__annotations__

    def checked_init(self, *args, **kwargs):
        bound = sig.bind(self, *args, **kwargs)
        for param, value in list(bound.arguments.items())[1:]:
            if param in hints and not isinstance(value, hints[param]):
                raise TypeError(f"{param} must be {hints[param].__name__}")
        original_init(self, *args, **kwargs)

    cls.__init__ = checked_init
    return cls

@enforce_types
class User:
    def __init__(self, name: str, age: int):
        self.name, self.age = name, age

try:
    User("Alice", 30, extra="oops")
except TypeError as e:
    print(f"TypeError: {e}")
$ python unknown_kwarg.py
TypeError: got an unexpected keyword argument 'extra'

sig.bind() raises before the type-checking loop runs, because extra is not a parameter __init__ declares. inspect.Signature.bind enforces the call shape for free.

Frequently Asked Questions

Do I need metaclasses in everyday Python?
Rarely. Most problems solved with metaclasses can be solved more simply with class decorators or __init_subclass__. Understand metaclasses to read framework code; reach for simpler tools first.
What's the difference between __new__ and __init__?
__new__ creates the object (returns the new instance). __init__ initializes it (receives the already-created instance). Override __new__ when you need to control object creation itself — like Singletons or immutable types.
Where do abstract base classes fit in?
ABC and @abstractmethod (built on the ABCMeta metaclass) are covered in the Abstraction lesson, since that's what you reach for day to day. This lesson focuses on writing custom metaclasses yourself, which is the rarer, more advanced skill.
What's the practical difference between a custom metaclass and __init_subclass__?
Both hook into class creation. A metaclass controls every class using it, including how the class object itself is built. __init_subclass__ only hooks subclass creation and is simpler to read — prefer it unless you specifically need metaclass-level control.