PythonMastery
advanced 22 min read · lesson 1 of 9 in Python Advanced

Metaclasses: Classes That Make Classes

1 · The lesson

read

A metaclass is the class of a class. If Account() creates an Account instance, then type(...) (or a subclass of type) creates Account itself. Once you understand that idea, the rest of this lesson follows from it.

Metaclasses are the lowest level of the Python object model. Many frameworks (Django ORM, SQLAlchemy declarative, Pydantic v1, abc.ABCMeta) use them to do work you don't see. You will almost never write a metaclass in application code: __init_subclass__ and class decorators cover most of the cases where people use metaclasses. But once you understand metaclasses, those frameworks become easier to read. Much of what they do is a __new__ override.


1. Classes Are Objects Too

Every value in Python has a type. Integers are instances of int. Strings are instances of str. And classes (the things you write with class Foo:) are instances of type.

python
class Foo:
    pass

print(type(42))                 # <class 'int'>
print(type("hi"))               # <class 'str'>
print(type(Foo))                # <class 'type'>     (Foo itself is an instance of type)
print(type(Foo()))              # <class '__main__.Foo'>

type is the default metaclass. When Python executes class Foo: ..., it calls type(...), which creates a class object and binds it to the name Foo. The class object is a normal value: you can pass it around, store it in a dict, attach attributes to it, return it from a function.

python
Foo.label = "registered"        # classes have a __dict__, just like instances
print(Foo.label)                # registered

classes = [int, str, Foo]       # store classes in containers
for cls in classes:
    print(cls.__name__)         # int / str / Foo
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

Foo = _AutoMock('Foo')

# Earlier blocks on this page that this one builds on, re-run quietly first.
_pm_earlier = [
    'class Foo:\n    pass\n\nprint(type(42))                 # <class \'int\'>\nprint(type("hi"))               # <class \'str\'>\nprint(type(Foo))                # <class \'type\'>     (Foo itself is an instance of type)\nprint(type(Foo()))              # <class \'__main__.Foo\'>',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

2. type(name, bases, namespace): The Three-Argument Form

type has two completely different uses depending on argument count:

  • type(obj), with one argument, returns the type of obj. This is the form you already know.
  • type(name, bases, namespace), with three arguments, creates a new class. This is what a class statement does when it runs.
python
# Build a class entirely without `class` syntax
def greet(self):
    return f"Hi, I'm {self.name}."

Person = type(
    "Person",                           # __name__
    (object,),                          # the base classes, always a tuple, even for one
    {                                   # the namespace dict holds methods and attributes
        "species": "Homo sapiens",
        "__init__": lambda self, name: setattr(self, "name", name),
        "greet": greet,
    },
)

p = Person("Hedy")
print(p.greet())                # Hi, I'm Hedy.
print(p.species)                # Homo sapiens
print(type(p))                  # <class '__main__.Person'>
print(type(Person))             # <class 'type'>

That Person is indistinguishable from one built with class Person:. Python builds the namespace dict by executing the class body and then calls type(name, bases, namespace). The class keyword is a shorter, more readable way to make this call ("syntactic sugar").


3. class Foo: ... Is Sugar: A Demonstration

To make the equivalence concrete:

python
class Account:
    rate = 0.05
    def __init__(self, balance):
        self.balance = balance
    def interest(self):
        return self.balance * Account.rate

# is roughly equivalent to:

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

def interest(self):
    return self.balance * Account.rate

Account = type("Account", (object,), {
    "rate": 0.05,
    "__init__": __init__,
    "interest": interest,
})

a = Account(1000)
print(a.interest())             # 50.0

Same object, same behaviour. The class block is a more readable way to spell it. This long form is the important idea: when you write a metaclass, you are changing what gets called instead of type(...).


4. Defining a Metaclass

A metaclass is a class that inherits from type. Specify it on a class with metaclass=:

python
class Meta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"Meta.__new__ creating class {name!r}")
        print(f"  bases = {bases}")
        print(f"  namespace keys = {list(namespace)}")
        cls = super().__new__(mcs, name, bases, namespace)
        return cls

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


class Foo(metaclass=Meta):
    x = 1
    def hello(self):
        return "hi"

# Output appears IMMEDIATELY at class-definition time:
# Meta.__new__ creating class 'Foo'
#   bases = ()
#   namespace keys = ['__module__', '__qualname__', '__firstlineno__', 'x', 'hello', '__static_attributes__']
#   (this list is from Python 3.13; 3.12 and earlier show only '__module__', '__qualname__',
#   'x' and 'hello', and 3.14 also adds '__classdictcell__' at the end)
# Meta.__init__ initialising class 'Foo'

f = Foo()                       # creating an *instance* doesn't trigger Meta again
print(f.hello())                # hi

The arguments to Meta.__new__ are exactly the arguments type itself receives:

  • mcs: the metaclass (Meta). By convention it is called mcs (or cls), not self.
  • name: the name of the class being created ("Foo").
  • bases: a tuple of base classes.
  • namespace: the dict produced by executing the class body.

Subclasses of a class with metaclass=Meta inherit the metaclass automatically, so you set it once at the top of the hierarchy.


5. Plugin Registry: A Realistic Metaclass

The standard metaclass example: every subclass of Plugin should auto-register itself in a central dict, so the framework can discover all plugins without an explicit list.

python
class Registry(type):
    plugins = {}

    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if bases:                       # skip the abstract base itself
            Registry.plugins[name] = cls
        return cls


class Plugin(metaclass=Registry):
    """Base for all plugins. It is never instantiated directly."""
    def run(self):
        raise NotImplementedError


class JSONExporter(Plugin):
    def run(self):
        return "exported as JSON"


class CSVExporter(Plugin):
    def run(self):
        return "exported as CSV"


print(Registry.plugins)
# {'JSONExporter': <class '__main__.JSONExporter'>,
#  'CSVExporter':  <class '__main__.CSVExporter'>}

for name, cls in Registry.plugins.items():
    print(name, "->", cls().run())

No register() calls anywhere. Importing the file is enough. The metaclass runs when each class is created and records every subclass automatically. Frameworks use this pattern because the person writing a plugin doesn't need any extra code to register it.


6. Enforcing Conventions: TypeError at Creation Time

A metaclass sees every method name before an instance exists. Reject anything that doesn't match your convention:

python
import re

SNAKE_CASE = re.compile(r"^_?_?[a-z][a-z0-9_]*_?_?$")

class SnakeCaseEnforcer(type):
    def __new__(mcs, name, bases, namespace):
        for attr_name, value in namespace.items():
            if callable(value) and not SNAKE_CASE.fullmatch(attr_name):
                raise TypeError(
                    f"{name}.{attr_name} violates snake_case convention"
                )
        return super().__new__(mcs, name, bases, namespace)


class Good(metaclass=SnakeCaseEnforcer):
    def do_thing(self): pass        # fine
    def __init__(self): pass        # allowed, because dunder names match the regex


# class Bad(metaclass=SnakeCaseEnforcer):
#     def DoThing(self): pass
# TypeError: Bad.DoThing violates snake_case convention

The error fires at class definition, not when anyone calls DoThing(). Failing early, at import time, is the main reason to use a metaclass for validation. A module with a bad name will not even load, so the tests fail at once.


7. __init_subclass__: The Modern Alternative

Python 3.6 added a hook that handles most "do something when a subclass is created" cases without a metaclass. Override __init_subclass__ on the parent class:

python
class Plugin:
    plugins = {}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.plugins[cls.__name__] = cls

    def run(self):
        raise NotImplementedError


class JSONExporter(Plugin):
    def run(self): return "json"


class CSVExporter(Plugin):
    def run(self): return "csv"


print(Plugin.plugins)
# {'JSONExporter': <...>, 'CSVExporter': <...>}
+ setup added so this can run · defines kwargs
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

kwargs = _AutoMock('kwargs')

This has the same behaviour as the metaclass version, without any type subclass. __init_subclass__ is a classmethod on the parent that runs every time a subclass is created. You can even pass keyword arguments through the class line:

python
class Plugin:
    def __init_subclass__(cls, *, category, **kwargs):
        super().__init_subclass__(**kwargs)
        print(f"registering {cls.__name__} in category {category}")


class JSONExporter(Plugin, category="export"):
    pass
# registering JSONExporter in category export
+ setup added so this can run · defines kwargs
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

kwargs = _AutoMock('kwargs')

Try __init_subclass__ first. Use a metaclass only when you need to customise the namespace dict, intercept attribute lookup on the class itself, or do something else that __init_subclass__ can't do.


8. __class_getitem__: A Short Note

Writing MyContainer[Foo] calls __class_getitem__ on the class MyContainer:

python
class Box:
    def __class_getitem__(cls, item):
        return f"Box of {item.__name__}"

print(Box[int])                 # Box of int

The standard library uses this hook for generic classes in typing and (since 3.9) for built-in containers like list[int]. (List[int] and Dict[str, int] from typing are not classes, so they use an ordinary __getitem__ method instead.) You will almost never write one yourself. The point is that square brackets after a class name are a normal method call.


9. The Honest Rule

If a class decorator or __init_subclass__ can do the job, do not write a metaclass. Metaclasses fight inheritance: the moment a class needs to combine two libraries that both use metaclasses, you get:

python
TypeError: metaclass conflict: the metaclass of a derived class must be a
(non-strict) subclass of the metaclasses of all its bases

There is no clean fix. You have to create a new metaclass by hand that subclasses both, and that only works if neither library prevents it. This is a real problem in real projects.

Use this priority order:

NeedUse
Add behaviour to a single classA regular decorator
React to every subclass being created__init_subclass__
Transform/validate attributes on a classClass decorator
Customise the namespace dict during creation__prepare__ on a metaclass
Need to override __call__ on the class itselfMetaclass
Combining two metaclassed frameworksDon't. Find another approach

10. Where You Will Meet Metaclasses

You won't write them often. You'll use them constantly:

  • Django ORM: class User(models.Model): uses ModelBase (a metaclass) to translate field declarations into the SQL schema, register the model in the app registry, and build the Manager.
  • SQLAlchemy declarative: DeclarativeMeta wires column descriptors into a mapper.
  • Pydantic v1: ModelMetaclass builds the validation/serialisation machinery from field annotations. (v2 moved most of this into Rust extension code, but the metaclass shell is still there.)
  • abc.ABCMeta: the metaclass behind abc.ABC. It tracks abstract methods and blocks instantiation of subclasses that haven't implemented them all.
  • enum.EnumMeta: turns class Color(Enum): RED = 1 into a singleton-per-value enum.

If you find yourself "reading the source of a framework to figure out why my class behaves strangely," look for a metaclass at the top of the hierarchy. The unusual behaviour very often comes from there.


Common Mistakes

1. Writing a metaclass when __init_subclass__ would do. See Section 7. The modern hook is almost always enough, and it doesn't cause metaclass conflicts in the inheritance tree.

2. Metaclass conflicts when subclassing across frameworks

python
class MyModel(SQLAlchemyBase, DjangoModel):    # this probably raises TypeError
    ...
+ setup added so this can run · defines SQLAlchemyBase, DjangoModel
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

SQLAlchemyBase = _AutoMock('SQLAlchemyBase')
DjangoModel = _AutoMock('DjangoModel')

Two libraries, two metaclasses, no common ancestor. There's no clean fix, and this is the main reason for the common advice "avoid metaclasses if you can".

3. Treating metaclass methods as instance methods

python
class Meta(type):
    def show(cls):
        print(f"I am {cls.__name__}")

class Foo(metaclass=Meta):
    pass

Foo.show()                      # I am Foo                (works because Foo is an instance of Meta)
Foo().show()                    # AttributeError          (a Foo object is not an instance of Meta)

Methods defined on the metaclass are callable on the class (because the class is an instance of the metaclass), not on instances of the class. This confuses almost everyone the first time they see it.

4. Expecting Meta.__init__ to run on instances of the class. It does not. Meta.__new__/Meta.__init__ run once, when the class is created. The class's own __init__ runs for every instance. They run at completely different times.

5. Forgetting super().__new__(mcs, name, bases, namespace). You must return a class object built by type.__new__, not some other value. Skipping the super() call breaks the type system in subtle ways (isinstance checks misbehave, MRO computation fails).


🎯 Your Turn: Build a Singleton

A singleton is a class that always returns the same instance: Config() and Config() should be the same object. The standard metaclass for this overrides __call__ on the metaclass so calling Config(...) is intercepted before __init__ runs.

Build it two ways:

1. As a Singleton(type) metaclass.
2. As a __new__ override on a base class.

Compare them, then decide which one you would use in real code.

python
# Metaclass version
class Singleton(type):
    # TODO 1: keep a dict that maps each class to its one instance
    # TODO 2: override __call__(cls, *args, **kwargs)
    #          - if cls already has an instance, return it
    #          - otherwise call super().__call__(...) and store the result
    ...


class Config(metaclass=Singleton):
    def __init__(self, name="default"):
        self.name = name


a = Config("first")
b = Config("second")
print(a is b)                   # True
print(a.name, b.name)           # first first   (__init__ ran only once)
Hint 1: Singleton.__call__ runs when you call the class The metaclass's __call__ is what fires when someone writes Config(...). Inside it, cls is Config. Store instances on the metaclass: cls._instances = {} on the metaclass itself, keyed by cls.
Hint 2: Only call super().__call__ once The first time a class is requested, you need super().__call__(*args, **kwargs) to construct it (this runs __init__). Cache the result. On subsequent calls, return the cached instance without calling super. That is what stops __init__ from running again.
Show full solution
python
# ----- Version A: metaclass -----
class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in Singleton._instances:
            Singleton._instances[cls] = super().__call__(*args, **kwargs)
        return Singleton._instances[cls]


class Config(metaclass=Singleton):
    def __init__(self, name="default"):
        self.name = name


a = Config("first")
b = Config("second")
print(a is b)                   # True
print(a.name)                   # first        (the second call skipped __init__)


# ----- Version B: __new__ override -----
class SingletonBase:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance


class ConfigB(SingletonBase):
    def __init__(self, name="default"):
        self.name = name


a = ConfigB("first")
b = ConfigB("second")
print(a is b)                   # True
print(a.name)                   # second       (__init__ STILL runs on every call!)
+ setup added so this can run · defines args, kwargs
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

args = _AutoMock('args')
kwargs = _AutoMock('kwargs')

Two important differences:

  • The metaclass version intercepts the call before __init__. __init__ runs exactly once, the first time. Later Config("anything") calls return the cached instance unchanged.
  • The __new__ version returns the cached instance, but Python still calls __init__ on it every time. b.name ends up "second" because the second call re-initialises the same object. Often this is a bug.

There is also a simpler approach: use a module. Python imports a module only once, so a module is already a singleton:

python
# config.py
name = "default"

def set_name(n):
    global name
    name = n

# everywhere else
import config
config.name             # one shared "instance", with no extra class code needed

Recommendation: prefer the module-as-singleton approach in real code. If you really need a class (because you need polymorphism, or you're matching an existing API), use the metaclass version, because it handles __init__ correctly. Use the __new__ version only when you can guarantee __init__ is idempotent.


What You Learned

  • Classes are objects, and their type is type (or a subclass of type, which is a metaclass).
  • type(name, bases, namespace) is the three-argument form that creates a class. class Foo: is a shorter way to make that call.
  • A metaclass customises class creation by overriding type.__new__, type.__init__, or type.__call__. The arguments mirror type's: name, bases, namespace dict.
  • The metaclass runs once, when the class is defined, not once per instance. A common confusion: Meta.__init__ is not the class's __init__.
  • __init_subclass__ is the modern Python 3.6+ hook that replaces metaclasses for most "react to subclasses" cases. Always try it first.
  • Class decorators cover most of the rest. Metaclasses cause problems with inheritance: combining two libraries that each use one is difficult.
  • Real frameworks using metaclasses: Django ORM, SQLAlchemy declarative, Pydantic v1, abc.ABCMeta, enum.EnumMeta. You'll use these constantly, but you'll write your own rarely.

Next: Descriptors & Properties, the protocol behind @property, @classmethod, methods themselves, and a hundred ORM field types.

Found this useful? Share it with a friend who’s learning Python.