Metaclasses & Introspection
Python has no separate "class compilation time". The class statement is an executable instruction: the interpreter runs the class body as an ordinary block of code, collects the resulting names into a namespace dict, and hands it to a constructor that builds the class object and binds the name to it. That object goes into a variable, is passed as an argument, and can be assembled entirely at runtime. The class of a class is called a metaclass; by default it is type, and the whole first half of the topic follows from one fact — a class is an instance of its metaclass, and a metaclass is an ordinary class inheriting from type.
The second half is about a program reading and modifying itself, and the distinction is asked for word by word: introspection reads (type, isinstance, dir, __dict__, the inspect module), reflection acts — it writes attributes by string name, calls methods, and constructs objects from names unknown when the code was written. The traps are worth naming upfront. type is two completely different calls under one name. __getattr__ fires only after the normal lookup has already failed, while __getattribute__ intercepts every access and almost always hits infinite recursion on the first attempt. A metaclass conflict comes not from inheritance but from the bases' metaclasses being incompatible. And most importantly — since __init_subclass__ and __set_name__ arrived, most of the jobs metaclasses used to be written for are solved without one.
Topic map
- Classes are objects —
classbuilds an object at runtime;type(Foo)is the metaclass, whileFoo.__bases__is an entirely different axis. - Introspection — reading an object's type and structure on the fly via
type,isinstance,dir,__dict__andinspect. - getattr and the attribute-access hooks — access by string name, the role of
default, and the difference between__getattr__and__getattribute__. - Reflection — writing attributes, calling a method by name, importing by string, and the price of that dynamism.
- type — the function and the class constructor — the one-argument form returns a type, the three-argument form builds a new class.
- The metaclass — a class of a class — how a metaclass is chosen and inherited, and why a metaclass conflict happens.
- The class-creation mechanics — the full chain from
__prepare__to__init__, and why__init_subclass__displaced metaclasses.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
| Calling the base class in the parentheses a metaclass | The inheritance axis (__bases__) gets mixed up with the creation axis (type(Foo)) — after that neither type(type) nor the metaclass conflict can be explained |
Expecting type(Foo) to return object | object is the top of inheritance, not the maker of classes; type(Foo) returns the metaclass, type by default |
Believing type(name, bases, ns) builds an instance | The three-argument form builds a new class — exactly what the class statement does internally |
Thinking a metaclass hook fires on every Foo() | A metaclass __new__/__init__ runs once, at class definition; instance creation is handled by Meta.__call__ |
Reading default in getattr as the success value | default substitutes only for AttributeError; any other exception raised inside a property flies straight out |
Implementing __getattribute__ via self.__dict__[name] | Touching self.__dict__ re-enters __getattribute__ — RecursionError; use object.__getattribute__ |
| Reaching for a metaclass to validate or register subclasses | That is exactly the niche __init_subclass__ and __set_name__ fill — a metaclass adds only the risk of a conflict |
What interviews check
The topic is marked advanced, but it opens with two short questions that filter out rote answers. The first is what a metaclass is, and the correct answer is phrased through instantiation, not inheritance: a metaclass creates class objects just as a class creates its instances. The second is in what sense a class is an object, and here the interviewer wants type(Foo) is type, Foo.__class__, and the ability to build a class at runtime. Next comes type with one and with three arguments — a question that looks syntactic but checks whether you understand that the class statement calls that very constructor.
The middle of the conversation is mechanics. You are asked to list what a metaclass receives and in what order, how __new__ differs from __init__, and when __init_subclass__ fires. The same block covers the difference between getattr(obj, name) and obj.name, the difference between __getattr__ and __getattribute__, and the recursion trap inside an interceptor. The closing question is almost always the same — where metaclasses are actually used. A strong answer names declarative framework APIs (abc.ABCMeta, enum.EnumType, Django's ModelBase) and immediately adds that application code almost always gets by with __init_subclass__, __set_name__, or a class decorator. Answering "metaclasses are needed for inheritance" or "for speed" ends the topic badly.