Metaclasses and class creation

Python · Expert · 16 min

ORMs, enums, ABCs and plugin registries customise class creation; you need this to read or build them.

Classes are objects too, and their type is their metaclass — type by default. type(name, bases, namespace) creates a class dynamically, which is exactly what a class statement does under the hood:

  • determine the metaclass (from metaclass= or the bases)
  • call Meta.__prepare__(name, bases) to get the namespace mapping
  • execute the class body in that namespace
  • call Meta(name, bases, namespace), i.e. Meta.__new__ then Meta.__init__
  • call __set_name__ on attributes that define it, then __init_subclass__ on the parent
  • apply class decorators

A metaclass can intercept any of these steps, and its methods apply to its classes the way normal methods apply to instances — Enum uses EnumType.__iter__ so you can loop over an enum class, and ABCMeta blocks instantiation of abstract classes.

Before reaching for one, try the lighter hooks, which cover the vast majority of real uses:

  • __init_subclass__(cls, **kwargs) on a base class runs for every subclass — registries, validation, keyword configuration (class Csv(Exporter, ext="csv"))
  • __set_name__ lets descriptors learn their attribute names
  • class decorators post-process a class (like @dataclass)

Metaclasses also don't compose well: a class can have only one, and combining bases with unrelated metaclasses raises a "metaclass conflict" TypeError. Use them when you must control the namespace (__prepare__), change how the class object itself behaves (operators or iteration on the class), or intercept instance creation via __call__. As Tim Peters put it, if you wonder whether you need them, you don't.

Example

class Exporter:
    registry: dict[str, type] = {}
    def __init_subclass__(cls, *, ext: str, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.ext = ext
        Exporter.registry[ext] = cls

class CsvExporter(Exporter, ext="csv"): pass
class JsonExporter(Exporter, ext="json"): pass
print(sorted(Exporter.registry))   # ['csv', 'json']

class Singleton(type):              # a metaclass: controls instance creation
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]
class Config(metaclass=Singleton):
    def __init__(self):
        print("loading config once")

print(Config() is Config())         # loading config once / True
print(type(Config).__name__, type(type).__name__)   # Singleton type
Point = type("Point", (), {"x": 0})  # what a class statement does
print(Point.__name__, Point().x)    # Point 0

Remember

  • Classes are instances of their metaclass, type by default
  • A class statement calls __prepare__, runs the body, then Meta(name, bases, ns)
  • Prefer __init_subclass__, __set_name__ or class decorators when they suffice

Watch out for

  • Using a metaclass for something __init_subclass__ or a decorator can do, creating metaclass conflicts later.

Check yourself

  1. What is `type(int)`?
  2. Which hook runs on a base class each time a subclass is defined, without a metaclass?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/reference/datamodel.html#metaclasses

Builds on

Unlocks

Nothing builds on this yet.