Backend / Python core / 08_metaclass.md

Metaclasses

Updated 4 interview angles 4 min read source
On this page6
  1. The mechanism
  2. When you genuinely need one
  3. Why you almost never need one
  4. Where it breaks in production
  5. Related
  6. Interview angle

Metaclasses

A metaclass is the class of a class. Every class you define is an instance of type, and type is what actually builds it — a metaclass is a subclass of type that gets to intervene in that construction. Interviewers ask about them to find out whether you know when not to use one, which is nearly always.

The mechanism

class Foo: ... is sugar for a call. Python evaluates the class body into a namespace dict, then calls the metaclass with the name, the bases and that dict:

python
def make(name, bases, ns):
    return type(name, bases, ns)

# These two are the same object shape.
class A:
    x = 1

B = type("B", (), {"x": 1})
print(type(A), type(B))        # <class 'type'> <class 'type'>

Hooking the call

A custom metaclass hooks that call. __new__ runs before the class exists and can rewrite the namespace; __init__ runs after; __call__ runs every time the class is called, which is how you intercept instance creation.

python
class Registry(type):
    plugins: dict[str, type] = {}

    def __new__(mcls, name, bases, ns):
        cls = super().__new__(mcls, name, bases, ns)
        if not ns.get("abstract", False):
            Registry.plugins[name.lower()] = cls    # runs at class definition
        return cls

class Plugin(metaclass=Registry):
    abstract = True

class CsvExport(Plugin): ...
class JsonExport(Plugin): ...

print(Registry.plugins)   # {'csvexport': ..., 'jsonexport': ...}

The point to make out loud: the registration happened at import time, when the class statement ran, with no instance involved. That is what a metaclass buys — code that runs when a class is defined.

When you genuinely need one

  • Collecting declared fields — the ORM pattern. Django models and old Pydantic walk the class namespace at definition to find Field objects and build the schema.
  • Enforcing rules at definition time — every subclass must define handle(), or a class attribute must have a certain type. Failing at import beats failing on first use.
  • Changing what isinstance means — __instancecheck__ and __subclasscheck__ live on the metaclass. abc.ABCMeta is the standard example, and it is why you can register a virtual subclass.
  • Singletons via __call__ — a metaclass that caches instances. Usually the wrong tool (a module-level instance is simpler), but it is the canonical demo.

Why you almost never need one

__init_subclass__ covers the common cases without a metaclass, and it is what to reach for first:

python
class Plugin:
    plugins: dict[str, type] = {}

    def __init_subclass__(cls, *, abstract: bool = False, **kw):
        super().__init_subclass__(**kw)
        if not abstract:
            Plugin.plugins[cls.__name__.lower()] = cls

class CsvExport(Plugin): ...
class Base(Plugin, abstract=True): ...

Same registry, no metaclass, and — the real reason — no metaclass conflicts. Two bases with different metaclasses cannot be combined unless one metaclass subclasses the other, and that error surfaces in whoever inherits from your class later, in a different codebase. A class decorator is the other lightweight option when you only need to touch the finished class.

The order of preference in an interview answer: class decorator or __init_subclass__ for nearly everything; a metaclass only when you must change instance creation (__call__), isinstance semantics, or rewrite the namespace before the class exists.

Where it breaks in production

  • Metaclass conflict on multiple inheritance. TypeError: metaclass conflict when a class inherits from two bases with unrelated metaclasses. The fix is a metaclass that subclasses both, which nobody wants to maintain.
  • Surprise at import time. Registration or validation that runs on import means importing the module has side effects, and a circular import now fails in the metaclass rather than somewhere readable.
  • Tooling goes blind. Type checkers and IDEs reason poorly about attributes a metaclass injects. Pydantic v2 moved to __init_subclass__ and a dataclass transform partly for this reason.
  • __call__ override hides construction. A caching __call__ means Foo() sometimes does not construct anything, which reads like a bug to the next engineer.

Interview angle 4

  • “What is a metaclass?” - the class of a class. type is the default; a custom metaclass customises class creation itself, running at class-definition time rather than instance creation.
  • “When do you actually need one?” - almost never in application code. Registering subclasses, enforcing rules at definition, ORM field collection and custom isinstance are the legitimate uses - and __init_subclass__ or a class decorator handles the first three more simply.
  • “__init_subclass__ or a metaclass?” - __init_subclass__. It covers most subclass customisation without metaclass conflicts under multiple inheritance, which is the failure that shows up in someone else’s codebase later. Follow-up: what can only a metaclass do? Override __call__ to intercept instance creation, and __instancecheck__ to change what isinstance means.
  • “Show me the class statement as a function call.” - type(name, bases, namespace). Being able to write that from memory is what convinces an interviewer you understand the mechanism rather than the vocabulary.