Intermediate Python 📂 Class and Object · 10 of 10 40 min read

Python Metaclasses Tutorial

Master Python metaclasses — the classes that create classes. Understand why "everything is an object," how type is the metaclass of every class, how to write your own metaclass to validate class definitions or auto-register subclasses, and when the modern __init_subclass__ hook is cleaner. Includes a cookie-cutter-factory analogy, three animated SVG diagrams (three-level hierarchy, class creation intercept, plugin registry), practical validator and registry examples, plus 10 golden rules.

Section 01

The Story That Explains Metaclasses

The Cookie Cutter Factory
Picture three layers in a bakery.

Layer 1 — the cookies. Individual cookies on a tray. Each one is a real object you can eat. These are your instances (like my_dog).

Layer 2 — the cookie cutters. Metal shapes — star, heart, moon. Each cutter stamps out many cookies of one shape. These are your classes (like Dog).

Layer 3 — the cookie-cutter factory. The machine at the back of the workshop that manufactures the cutters themselves. Every cutter — star, heart, moon — was produced by this one machine. This is a metaclass (like type).

Now the twist. In Python, everything is an object — including classes. If a class is an object, something must have created it. That "something" is a metaclass. Python's default metaclass is type — the machine that makes every class you've ever written, from Dog to Vector to BankAccount.

Write your own metaclass, and you customise how classes themselves get created. Add validation that every subclass must follow. Auto-register every plugin the moment it's defined. Inject methods into every class in a hierarchy. It's the deepest hook Python gives you.
🏭
The Core Insight

Classes create objects. Metaclasses create classes. Just as an object's behaviour is defined by its class, a class's behaviour is defined by its metaclass. Python's default metaclass type is invisible until you replace it with your own — then every class that uses your metaclass goes through your custom creation logic.


Section 02

Everything Is an Object — Including Classes

Python has one central truth that unlocks metaclasses: classes are objects too. You can pass them around, store them in lists, assign new attributes to them, and — the key part — they were themselves created by another class. That creator class is type.

class Dog:
    pass

buddy = Dog()

# buddy is an instance of Dog
print(type(buddy))         # <class '__main__.Dog'>

# Dog is an instance of type — that's the metaclass level
print(type(Dog))           # <class 'type'>

# type is itself an instance of type — the recursion stops here
print(type(type))          # <class 'type'>

# Classes have __class__ pointing to their metaclass
print(Dog.__class__)       # <class 'type'>
OUTPUT
<class '__main__.Dog'> <class 'type'> <class 'type'> <class 'type'>

Animated Diagram — The Three-Level Hierarchy

Trace the "what is it an instance of?" arrow at each level. The individual dog is an instance of the Dog class. The Dog class is an instance of type. type is an instance of itself — the top of the chain.

THREE LEVELS  ·  OBJECT ← CLASS ← METACLASS
INSTANCE (object) buddy = Dog() a real dog you can pet type(buddy) CLASS class Dog: ... the blueprint for dogs type(Dog) METACLASS type the factory that makes classes type(type) == type Layer 1 the cookie Layer 2 the cutter Layer 3 the factory

Every object has a class. Every class has a metaclass. type is where the recursion stops — it's the metaclass of itself.


Section 03

Creating a Class Dynamically With type()

Because type is the metaclass, calling it with the right arguments creates a class from scratch — no class keyword needed. This is the machinery Python uses internally every time you write a class.

# These two forms produce IDENTICAL classes — Python translates one into the other

# Form 1 — the class keyword you already know
class Dog:
    species = "Canis familiaris"
    def bark(self):
        return f"{self.species} says woof"


# Form 2 — same thing, built by calling type() directly
def bark(self):
    return f"{self.species} says woof"

Dog2 = type(
    "Dog2",                                    # class name
    (),                                          # base classes
    {"species": "Canis familiaris", "bark": bark}    # namespace
)

# Both work identically
print(Dog().bark())       # Canis familiaris says woof
print(Dog2().bark())      # Canis familiaris says woof
print(type(Dog) is type(Dog2))   # True — both classes have type() as their metaclass
OUTPUT
Canis familiaris says woof Canis familiaris says woof True
💡
The Three Arguments to type

When called with three arguments, type creates a class: name (string), bases (tuple of parent classes), and namespace (dict of attributes and methods). Every class you've ever written with the class keyword ends up as this same call under the hood.


Section 04

Your First Custom Metaclass

To write a metaclass, subclass type. Override its __new__ or __init__ to run code every time a class using your metaclass is defined. Then attach it via metaclass=YourMeta in the class header.

class LoggingMeta(type):
    """A metaclass that announces every class defined with it."""

    def __new__(mcs, name, bases, namespace):
        print(f"[LoggingMeta] creating class {name!r} with {len(namespace)} attrs")
        # Delegate the real creation to type
        cls = super().__new__(mcs, name, bases, namespace)
        # Free to modify the class here — inject methods, add attributes...
        cls.created_at = "2026-07-12"
        return cls


class Cat(metaclass=LoggingMeta):
    def meow(self):
        return "meow"


class Dog(metaclass=LoggingMeta):
    def bark(self):
        return "woof"


# The metaclass injected created_at into every class
print(Cat.created_at)    # 2026-07-12
print(Dog.created_at)    # 2026-07-12

# type() confirms the metaclass
print(type(Cat))         # <class '__main__.LoggingMeta'>
print(type(Dog))         # <class '__main__.LoggingMeta'>
OUTPUT
[LoggingMeta] creating class 'Cat' with 3 attrs [LoggingMeta] creating class 'Dog' with 3 attrs 2026-07-12 2026-07-12 <class '__main__.LoggingMeta'> <class '__main__.LoggingMeta'>

Animated Diagram — The Metaclass Intercepts Class Creation

Watch what happens when Python encounters class Dog(metaclass=LoggingMeta):. Before the class object even exists, Python calls LoggingMeta.__new__. Your metaclass sees the raw name, bases, and namespace — and can validate, decorate, or reject before handing back the finished class.

METACLASS INTERCEPT  ·  CLASS DEFINITION → METACLASS → FINAL CLASS
WHAT YOU WRITE class Dog(metaclass=LoggingMeta):     def bark(self): return "woof" Python calls METACLASS.__new__ RUNS LoggingMeta.__new__(mcs, "Dog", (), {"bark": bark, ...}) cls = super().__new__(mcs, name, bases, ns) cls.created_at = "2026-07-12" return cls FINISHED CLASS  ·  ENRICHED BY METACLASS Dog.bark(), Dog.created_at = "2026-07-12" Dog is now an instance of LoggingMeta

The metaclass runs at class definition time — before any instances exist. It's your one chance to shape the class itself.


Section 05

What Do Metaclass __new__, __init__, and __call__ Control?

🏭
Meta.__new__
runs at class creation
Called before the class exists. Receives mcs, name, bases, namespace. Return the class. Best place to validate the namespace or inject attributes before the class is finalised.
🛠️
Meta.__init__
runs after class creation
Called after __new__. Receives the created class as cls. Best place to register the class in a global registry or perform post-processing.
💸
Meta.__call__
runs on every instantiation
Called every time you write MyClass(...). Controls how instances are created — this is what powers metaclass singletons and instance caches.
class Meta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"  [Meta.__new__]  creating class {name!r}")
        return super().__new__(mcs, name, bases, namespace)

    def __init__(cls, name, bases, namespace):
        print(f"  [Meta.__init__] class {name!r} is ready")
        super().__init__(name, bases, namespace)

    def __call__(cls, *args, **kwargs):
        print(f"  [Meta.__call__] creating an instance of {cls.__name__}")
        return super().__call__(*args, **kwargs)


print("Defining Dog:")
class Dog(metaclass=Meta):
    def __init__(self, name):
        self.name = name

print("\nCreating buddy:")
buddy = Dog("Buddy")
OUTPUT
Defining Dog: [Meta.__new__] creating class 'Dog' [Meta.__init__] class 'Dog' is ready Creating buddy: [Meta.__call__] creating an instance of Dog
🔑
Timing Is Everything

Meta.__new__ and Meta.__init__ run once per class — the moment Python encounters the class statement. Meta.__call__ runs every time you instantiate that class. This is the difference between "shape the class" and "shape each instance."


Section 06

Practical Example 1 — Enforcing Class-Design Rules

A metaclass that validates every class it produces. Here we enforce that every public method's name must be lowercase — a small stand-in for real coding standards you might want to enforce across a codebase.

class SnakeCaseMeta(type):
    """Every public method must be snake_case (lowercase)."""

    def __new__(mcs, name, bases, namespace):
        for attr_name, attr_value in namespace.items():
            # Only check callable, public attributes
            if callable(attr_value) and not attr_name.startswith("_"):
                if not attr_name.islower():
                    raise TypeError(
                        f"In class {name!r}: method {attr_name!r} "
                        "must be lowercase (snake_case)"
                    )
        return super().__new__(mcs, name, bases, namespace)


# ── This class is fine — all methods are snake_case ──
class Good(metaclass=SnakeCaseMeta):
    def do_something(self):
        return "ok"

    def save_to_disk(self):
        return "saved"

print("Good class created successfully")


# ── This class violates the rule — refused at DEFINITION time ──
try:
    class Bad(metaclass=SnakeCaseMeta):
        def DoSomething(self):        # PascalCase — not allowed
            return "nope"
except TypeError as e:
    print("Blocked:", e)
OUTPUT
Good class created successfully Blocked: In class 'Bad': method 'DoSomething' must be lowercase (snake_case)
🏆
Why This Is Powerful

The check runs the instant Python parses the class body — before any code that uses the class ever runs. It's not a linter you have to remember to invoke; it's structural enforcement built into the class system. This is how frameworks like Django and SQLAlchemy enforce their conventions.


Section 07

Practical Example 2 — Auto-Registry of Subclasses

A metaclass that quietly keeps track of every subclass ever defined. Add a new plugin, and it auto-registers itself — no manual bookkeeping. This is the pattern behind plugin systems, ORM model discovery, serializer registries, and much more.

class PluginRegistry(type):
    """A metaclass that auto-registers every subclass into a shared registry."""

    registry = {}    # class-level dict — one for the entire metaclass

    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        # Don't register the base "Plugin" itself — only its children
        if bases:
            PluginRegistry.registry[name] = cls
            print(f"[registry] Registered {name}")


# The base class — not registered because it has no bases
class Plugin(metaclass=PluginRegistry):
    def run(self, msg):
        raise NotImplementedError


# Every subclass is automatically added to the registry — no manual work
class EmailPlugin(Plugin):
    def run(self, msg):
        return f"EMAIL: {msg}"

class SMSPlugin(Plugin):
    def run(self, msg):
        return f"SMS: {msg}"

class PushPlugin(Plugin):
    def run(self, msg):
        return f"PUSH: {msg}"


# The registry now contains every plugin — dispatch by name
print()
print("All registered plugins:", list(PluginRegistry.registry))

for plugin_name, plugin_cls in PluginRegistry.registry.items():
    plugin = plugin_cls()
    print(f"  {plugin_name} → {plugin.run('hello')}")
OUTPUT
[registry] Registered EmailPlugin [registry] Registered SMSPlugin [registry] Registered PushPlugin All registered plugins: ['EmailPlugin', 'SMSPlugin', 'PushPlugin'] EmailPlugin → EMAIL: hello SMSPlugin → SMS: hello PushPlugin → PUSH: hello

Animated Diagram — The Registry Grows With Each Subclass

AUTO-REGISTRY  ·  EACH SUBCLASS DEFINITION ADDS A ROW
class EmailPlugin(Plugin): def run(self, msg): ... class SMSPlugin(Plugin): def run(self, msg): ... class PushPlugin(Plugin): def run(self, msg): ... METACLASS PluginRegistry __init__ intercepts each subclass adds to registry REGISTRY PluginRegistry.registry "EmailPlugin": ... "SMSPlugin": ... "PushPlugin": ... no manual registration every subclass adds itself

Every subclass triggers PluginRegistry.__init__ and adds itself. No decorator, no manual registration — the metaclass does it automatically.


Section 08

The Modern Alternative — __init_subclass__

Python 3.6 introduced __init_subclass__, which covers most real use cases for metaclasses without the ceremony. If all you need is "run some code every time a subclass is defined," this is almost always the better choice.

# ── SAME auto-registry pattern — no metaclass needed ──────
class Plugin:
    registry = {}

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


class EmailPlugin(Plugin):
    def run(self, msg): return f"EMAIL: {msg}"

class SMSPlugin(Plugin):
    def run(self, msg): return f"SMS: {msg}"

print(list(Plugin.registry))    # ['EmailPlugin', 'SMSPlugin']
🛡️ Metaclass
Requires understanding of the metaclass concept
Verbose: name, bases, namespace parameters
Metaclass conflicts when mixing classes
Powerful: full control over class creation
Right choice for framework authors
✅ __init_subclass__
Just a method on the base class
Takes cls, **kwargs — minimal
No metaclass conflicts
Sufficient for 90% of real needs
Right choice for application authors
💡
The Rule of Thumb

"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't." — Tim Peters. Try __init_subclass__ or a simple class decorator first. Reach for a metaclass only when those genuinely don't fit — typically when you need to control the class namespace itself before the class is built.


Section 09

When to Use a Metaclass (and When Not To)

Framework Boundaries
Django models, SQLAlchemy declarative base, Pydantic — the metaclass converts your class definition into rich internal representations (columns, fields, validators).
framework internals
Enforce Coding Standards
Every class in a hierarchy must implement certain methods, follow a naming pattern, declare required attributes. The check runs at class definition, before any use.
structural validation
Deep Class Modification
Rewrite the namespace before the class exists — collect specially-marked methods, inject helpers, register slots. Something a plain decorator can't do cleanly.
namespace transformation
Auto-Registration
Use __init_subclass__ instead. Same behaviour, none of the metaclass complexity, no chance of metaclass conflicts when mixing with other hierarchies.
use __init_subclass__
Singletons
Use a module (thread-safe, imported once) or override __new__ on the class. A metaclass just to make one class singleton is overkill.
use a module
"Cool" or "Clever" Code
Metaclasses have a real cognitive cost for every reader. If your goal isn't a genuine framework feature, the readability tax isn't worth it.
prefer boring code

Section 10

Common Mistakes (and Fixes)

MistakeWhat Goes WrongFix
Forgetting to return from Meta.__new__ The class definition evaluates to None Always end __new__ with return cls (the created class)
Using self instead of mcs / cls Confusing. In __new__, first arg is mcs (the metaclass); in __init__/__call__, it's cls (the class being created) Match convention: def __new__(mcs, name, bases, ns), def __init__(cls, ...)
Modifying namespace then not passing the modified version to super().__new__ Your changes are silently dropped Pass the same (or updated) namespace: super().__new__(mcs, name, bases, namespace)
Metaclass conflict in multiple inheritance TypeError: metaclass conflict when parents have different metaclasses Choose one metaclass that's a subclass of the others, or use __init_subclass__ instead
Putting logic in Meta.__init__ that needs pre-class-creation info The class already exists — some kinds of modification are too late Do namespace-level work in __new__. Reserve __init__ for post-processing
Registering the base class alongside subclasses The registry contains the abstract base too — polluting downstream lookups Check if bases: — only register when there's a real parent (not the base itself)
Reaching for a metaclass when a class decorator would do Unnecessary complexity that other developers must decode Prefer decorators, __init_subclass__, or plain composition. Metaclass is the last resort

Section 11

Quick Reference

TaskSyntaxNotes
Default metaclasstypeEvery class not otherwise marked uses type
Get an object's classtype(obj)Same as obj.__class__
Get a class's metaclasstype(SomeClass)Same as SomeClass.__class__
Create class dynamicallytype(name, bases, namespace)Three-argument form of type()
Custom metaclassclass MyMeta(type): ...Subclass type
Attach to a classclass C(metaclass=MyMeta):Keyword in class header
Intercept class creationdef __new__(mcs, name, bases, ns):Runs once per class definition
Post-process a classdef __init__(cls, ...):Class exists here; add to registries, etc.
Intercept instantiationdef __call__(cls, *a, **kw):Runs every time MyClass(...) is called
Modern alternativedef __init_subclass__(cls, **kw):No metaclass needed for most cases

Section 12

Golden Rules

🏭 Metaclasses — Non-Negotiable Rules
1
Classes are objects. Every class is an instance of some metaclass. Python's default is type. Understanding this one sentence unlocks the entire metaclass system.
2
A metaclass is just type with your own __new__, __init__, or __call__ methods. Subclass type, add the hooks you need, attach with metaclass=.
3
Meta.__new__ runs before the class exists — best place for namespace validation or attribute injection. Meta.__init__ runs after — best place for registration and post-processing. Meta.__call__ runs on every instantiation — controls how instances are made.
4
Prefer __init_subclass__ when all you need is "do something for every subclass." It covers 90% of real metaclass use cases with none of the complexity, and never causes metaclass conflicts.
5
Prefer a class decorator over a metaclass when the modification is per-class, not per-hierarchy. Decorators compose cleanly; metaclasses don't.
6
Metaclass conflicts are the most painful bug metaclasses cause. If a subclass's parents each use a different metaclass, Python refuses to build the class. Design metaclass hierarchies deliberately; don't stack them casually.
7
Metaclasses have a real cognitive tax. Every reader has to understand another layer of indirection. If your code isn't a framework or library, that tax is usually not worth paying.
8
Convention: use mcs as the first parameter of __new__ (metaclass "self"), and cls for __init__/__call__ (the class being processed). It matches what every experienced Python developer expects.
9
When registering subclasses in __init__, check if bases: first — otherwise the abstract base class registers itself too, polluting your lookup table.
10
When in doubt, ask three questions. Do I need to modify the class namespace itself? → metaclass. Do I just want to run code per subclass?__init_subclass__. Do I want to transform one specific class? → class decorator. Reach for the simplest tool that solves the problem.
You have completed Class and Object. View all sections →