Articles
Python Metaclass Interview Questions: Class Creation and __init_subclass__
Trace Python class creation, choose __init_subclass__ or a metaclass, and test cooperative keywords, metaclass conflicts, and __classcell__ failures.
A Python metaclass interview question often starts with “What is a metaclass?” and becomes interesting only when you must predict a class-creation log. Does __set_name__ run before __init_subclass__? Does a class decorator run before the metaclass returns? Can two library base classes be combined without rewriting either library?
The useful answer is a sequence plus a design choice. Identify the information each hook needs, choose the earliest hook that can safely do the work, and show where cooperation can fail. A subclass registry rarely needs every capability of a custom metaclass. Practice that distinction with Implement Python feature and resolver metaprogramming.
Evidence boundary: Official behavior comes from Python's 3.14 language reference and PEPs 487 and 3115. The logs, registry policies, and repair exercises are original teaching material checked on CPython 3.12.14. They are not candidate reports or claims about interview frequency. The __classcell__ discussion is explicitly a CPython implementation detail.
What is being created: a class or an instance?
Start with three objects: the metaclass, a class built by that metaclass, and an instance built by calling the class. In the ordinary case, type creates classes. A custom metaclass derives from type and can customize class creation. Calling the resulting class later is a separate operation that normally constructs an instance.
For an original example, suppose Meta builds Report, and report = Report() creates a report instance. type(Report) is Meta; type(report) is Report. These expressions answer different questions. Saying that the metaclass is “the parent of every report” blurs a type relationship with ordinary inheritance.
Ask which event the requirement concerns. Validating that a declared class contains a required field is definition-time work. Checking that a particular report instance contains a valid customer identifier is instance-time work. One may be enforced through class hooks; the other requires an appropriate construction or validation path.
This distinction also changes failure timing. A definition-time exception can stop a module import before any instance exists. That might be desirable for an invalid plugin declaration, but a network call during class creation can make importing the application depend on a remote service. Our recommendation is to keep registration and declaration checks local and defer operational work to an explicit startup phase.
Can you trace the class-creation order?
The language reference describes namespace preparation, body execution, and class creation. When a metaclass delegates to type.__new__, that operation invokes applicable __set_name__ methods and the inherited __init_subclass__ hook before returning. Decorators then receive the created class. Python data model.
Run this original complete trace:
log = []
class Named:
def __set_name__(self, owner, name):
log.append("set_name")
class Base:
def __init_subclass__(cls, **kwargs):
log.append("init_subclass")
super().__init_subclass__(**kwargs)
class Meta(type):
@classmethod
def __prepare__(mcls, name, bases, **kwargs):
log.append("prepare")
return {}
def __new__(mcls, name, bases, namespace, **kwargs):
log.append("meta.new.before")
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
log.append("meta.new.after")
return cls
def __init__(cls, name, bases, namespace, **kwargs):
log.append("meta.init")
super().__init__(name, bases, namespace)
def mark(cls):
log.append("decorator")
return cls
@mark
class Report(Base, metaclass=Meta):
log.append("body")
field = Named()
print(log)
The expected order is prepare, body, meta.new.before, set_name, init_subclass, meta.new.after, meta.init, decorator. The fixture intentionally logs creation of Report, rather than earlier setup classes, so the sequence is easy to inspect.
The key observation is the middle of the log. The descriptor and subclass hook execute while Meta.__new__ is waiting for super().__new__ to return. An answer that places both hooks after meta.new.after has misunderstood which call owns them.
Now add a print in Report.__init__ and instantiate Report. That message belongs to a later instance-construction event. It should not appear in the class-definition log unless you deliberately create an instance during definition or decoration.
When is init_subclass the better tool?
PEP 487 introduced simpler hooks for common class-definition customization, including subclass initialization and naming class attributes. Its rationale includes reducing unnecessary metaclass conflicts. That makes __init_subclass__ a strong first candidate for inherited validation or registration after a subclass exists. PEP 487.
Consider this original plugin registry:
registry = {}
class Plugin:
def __init_subclass__(cls, *, key=None, **kwargs):
super().__init_subclass__(**kwargs)
if key is not None:
if key in registry:
raise ValueError(f"duplicate plugin key: {key}")
registry[key] = cls
class CsvPlugin(Plugin, key="csv"):
pass
assert registry["csv"] is CsvPlugin
The hook receives the newly created subclass as cls. It does not initialize each CsvPlugin instance. The registry can store a class without constructing a client, opening a file, or performing a query.
The duplicate-key rule is an original application policy. Python does not require that every subclass registry reject duplicates. State whether redefinition during development should replace an entry, be ignored, or fail. Otherwise the implementation's incidental dictionary assignment becomes an undocumented plugin policy.
Registration also has a failure boundary. In this example, delegation happens before the registry is changed. If a later declaration check is required, perform it before publishing the entry. A partially initialized class in a global registry can be observed by other initialization code even though its definition ultimately fails.
How do class keywords cooperate across mixins?
A useful original follow-up combines key="csv" with audited=True. Let the plugin hook consume key; let an audit mixin consume audited; let both forward remaining keywords through super().__init_subclass__(**kwargs).
The rule is ownership: each hook handles its own option and delegates the rest. Passing every original option unchanged to every base can leave an unexpected keyword at object.__init_subclass__. Dropping all remaining options avoids that error by silently disabling another mixin's feature. Neither is a correct cooperative design.
Trace the actual MRO, then log each consumed keyword. Assert that the plugin registry contains the class and that the audit flag is set. Finally, pass audtied=True with the deliberate spelling mistake and verify a definition-time error. Silently accepting unknown options would make the broken configuration harder to discover.
Do not call each named base hook directly to “make sure everything runs.” That approach can bypass the cooperative order or invoke shared work twice in a diamond. The objective is one chain in which every participant agrees on keyword consumption and delegation.
For a reusable library, document the class options and test compositions with another cooperative base. Testing the hook only in a single-inheritance example proves too little about whether users can combine it safely.
When do you need prepare or a metaclass?
__init_subclass__ operates after the class has been created. If the requirement is to control the mapping used while the body executes, that is earlier work. PEP 3115 describes __prepare__ as the namespace-preparation hook. PEP 3115.
Suppose an original schema exercise requires rejecting a repeated field name in the class body. Once a normal dictionary has overwritten the first assignment, a later hook sees only the final binding. A custom namespace mapping can observe each assignment and reject the second one at the moment it occurs.
That requirement has costs. Class bodies assign ordinary names, compiler-created entries, functions, and annotations. A mapping that rejects every repeated name may also prohibit intentional reassignment. Define the permitted declaration language rather than implementing a broad restriction and discovering framework incompatibilities later.
If the requirement is simply to preserve declaration order, ask whether normal ordered dictionaries already suffice on the supported Python version. A metaclass written solely to rediscover available behavior adds an unnecessary compatibility surface.
A class decorator is another option for an explicit transformation of selected classes. Unlike an inherited hook, it requires opting in at each decorated definition. Choose based on whether the policy belongs to every subclass or to a few marked classes; do not choose based on which syntax looks more advanced.
Why do unrelated metaclasses conflict?
Use this original selection puzzle:
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
class CombinedMeta(MetaA, MetaB):
pass
class Combined(A, B, metaclass=CombinedMeta):
pass
Defining class Broken(A, B): pass raises a metaclass conflict: neither unrelated candidate metaclass is a subclass of the other. The explicit combined metaclass satisfies the type-selection relationship in this minimal example.
That does not prove that two real framework metaclasses compose correctly. One may transform the namespace in a way the other does not expect. One may omit super(), preventing the other from running. Another may require a special prepared mapping. Resolving selection is only the first obligation.
A strong repair answer names both metaclasses, lists their hooks and assumptions, and tests a class that exercises each framework's behavior. Do not demonstrate an empty CombinedMeta over empty parents and claim that all production frameworks can now be combined safely.
What breaks if you discard classcell?
A namespace-filtering metaclass might retain only public names. That appears tidy until a method uses zero-argument super(). CPython can insert a __classcell__ entry that must reach type.__new__; discarding it can produce a class-definition RuntimeError. This is documented as an implementation detail in the data model.
The original repair exercise is to begin with a working subclass method using super(), introduce a metaclass that removes every double-underscore namespace entry, and observe the failure. Restore the special entry and rerun. Preserve other required metadata too; the safer design is a narrowly defined transformation rather than an unrestricted namespace purge.
Explain why replacing super() throughout the application is a poor first repair. The metaclass changed the construction contract, so removing a language feature from every method works around the wrong boundary. A minimal regression should keep the method unchanged and prove that class creation and its inherited call both succeed.
Sources and Further Reading
- Python 3.14 data model and class creation
- PEP 487: Simpler customization of class creation
- PEP 3115: Metaclasses in Python 3000
Sources checked October 1, 2026. Original fixtures were checked on CPython 3.12.14; custom metaclasses can introduce additional behavior outside these examples.