How do you decide between a metaclass and a class decorator for enforcing a rule across many classes?
answer
- Two ways to change a class after its body
- One is inherited, one is written down
- What happens when two bases disagree
- Which one a type checker understands
- Escalate only when omission must be impossible
basics
~20 sDecide on reach and cost. A metaclass is inherited, so it governs every future subclass and constrains inheritance; a class decorator touches only the class it is written on, but is visible and composable. Default to the decorator.
solid answer
~50 sThe axes are reach, conflict, visibility and cost. A metaclass propagates to every subclass automatically, which is exactly right when the invariant must hold for classes you will never see — and exactly wrong when it forces itself on subclasses that do not want it. A metaclass also constrains inheritance: combining two bases with unrelated metaclasses raises a `TypeError` metaclass conflict, so publishing a base with a custom metaclass exports that constraint to every consumer. A class decorator is applied where it is written, composes with other decorators, is visible at the definition site, and static type checkers model it far better than runtime class synthesis. My default is the decorator; I escalate to a metaclass only when the rule must be impossible to omit and must run at class-creation time, and I keep that machinery in one documented place.
code
python · 19 linesRULES = {}
def register(cls):
RULES[cls.field] = cls
return cls
@register
class DepartureRule:
field = "departure"
@register
class GateRule:
field = "gate"
print(sorted(RULES))go deeper
Know that both a class decorator and a metaclass can change a class after its body runs, and that the decorator is the one you will meet far more often. Being able to read @something above a class and say what it receives is enough here.
Explain the mechanical difference: a decorator receives the finished class and returns a class, affecting only where it is written, while a metaclass governs the class and every subclass. Name a stdlib decorator that transforms a class as your example.
Argue the tradeoff with real consequences: metaclass conflicts constrain your users' inheritance, runtime synthesis defeats type checkers, and import-time auto-registration drives cold-start and eager-import problems. Show you have chosen the smaller tool at least once.
Own the standard for the codebase: where such machinery may live, what must be true before anyone escalates to a metaclass, and how an existing one is migrated off without two divergent implementations of the same rule running side by side.
This is a design question, so answer it with axes rather than a verdict. ## The four axes **Reach.** A metaclass is inherited: define one on a base and every subclass, forever, is created through it — including subclasses written by people who never read your module. A class decorator applies to exactly the class it decorates and nothing below it. If somebody must be able to *forget* the rule and still be correct, the decorator is wrong. If somebody must be able to *opt out*, the metaclass is wrong. **Conflict.** Python requires a single most-derived metaclass among a class's candidate and all its bases. Two bases with unrelated custom metaclasses cannot be combined at all; the class statement fails with a `TypeError` about a metaclass conflict, and the only fix is a third metaclass deriving from both. Publishing a base class with a custom metaclass therefore exports a constraint: your users can no longer freely mix your base with an abstract base class from `abc`, an enumeration, or any other library's metaclassed type. Decorators have no such interaction — they compose in any order and any combination. **Visibility and tooling.** A decorator is written on the line above the class; a metaclass may sit three inheritance levels away in another package. Type checkers, IDE navigation and refactoring tools understand decorators — that is why the stdlib's own class transformations, such as `dataclasses.dataclass` and `functools.total_ordering`, are decorators rather than metaclasses. Attributes a metaclass synthesises at runtime are invisible to every static tool your team relies on. **Cost and timing.** Both run at import. A metaclass runs for every subclass in the process, which is normally negligible, but the *pattern* it enables — auto-registration at class-creation time — is not: registration only works if the defining module has been imported, so teams end up importing everything eagerly at startup. ## A worked scenario A flight-schedule differ has a growing family of comparison-rule classes, each of which must appear in a registry keyed by the field it compares. Two ways to guarantee that: * A metaclass on the rule base class registers every subclass as it is created. Nobody can define a rule that is not registered — that is real value, because the failure mode of a missing rule is silent. A clock-skew artefact that survived a release precisely because one rule class was never registered is the argument that wins this debate in the room. * A `@register` class decorator on each rule. Visible, greppable, trivially testable, composes with anything — and omittable, which is the whole problem. Both share a hidden cost. Registration at class-creation time means the registry is only complete once every rule module has been imported, so the service imports its whole rule tree at startup and pays a 45-second cold start for it. That points at the third option, which is often the right one: register explicitly from data — a manifest or an entry-point style declaration — and import each rule module lazily when the registry first needs it. Neither the metaclass nor the decorator is the interesting choice at that point; the import strategy is. ## The middle ground, and when to leave Modern Python offers a subclass-creation hook on the base class itself, which covers a large share of what metaclasses were historically written for without the conflict or the inheritance-tree pollution. When a metaclass in a codebase does nothing but react to subclass creation, migrating it to that hook is usually a straight win. That hook belongs to a sibling topic, so treat it here only as the reason to be suspicious of an existing metaclass. ## Migrating off a published metaclass Separate what the metaclass does into class-creation work and call-time interception. Move the class-creation work into a decorator or a subclass hook, keep the metaclass as a thin shim that calls the same function so both paths behave identically, convert call sites, then delete the shim. Do it in that order and no class is ever governed by two divergent implementations of the same rule. ## What I actually say in an interview Default to the class decorator. Escalate to a metaclass only when three things are simultaneously true: the rule must be impossible to omit, it must take effect at class-creation time, and no subclass hook suffices. When I do escalate, the metaclass lives in one module, is documented at the base class, and its behaviour is tested directly rather than inferred from the classes it governs. And I treat "we can be clever here" as a cost, not a benefit: the reader who meets that class at 3 a.m. is the person paying for it.
- What exactly fails when two base classes have unrelated custom metaclasses?Class creation itself. Python resolves the most derived metaclass among the explicit `metaclass=` candidate and the metaclasses of every base; if none of them is a subclass of all the others, it raises a `TypeError` reporting a metaclass conflict, and the class statement never completes. The fix is a third metaclass inheriting from both — which someone has to write, understand and maintain, which is the real cost of publishing a metaclassed base.
- Why are the standard library's own class transformations written as decorators?Because a decorator is visible where the class is defined, composes with other decorators, and does not touch the inheritance tree, so it cannot conflict with whatever else a user's class inherits from. It is also the form static analysers can follow — a decorator receives a class and returns one, which type checkers can model, while attributes conjured at runtime by a metaclass are invisible to them.
- How would you migrate a published base class off its custom metaclass without a flag day?Split the metaclass's work into class-creation work and call-time interception. Move the creation work into a decorator or a subclass-creation hook and have the metaclass call that same function, so both paths run identical code. Convert consumers, verify the shim is doing nothing unique, then delete it. The invariant is that no class is ever governed by two divergent implementations of the same rule.
saying these in an interview costs you the question
- Reaches for a metaclass whenever a class needs setup
- Thinks a class decorator cannot inspect the class's bases
- Ignores metaclass conflicts when publishing a base class
- Assumes a decorator applies to subclasses automatically
- Treats extra indirection as free because it is powerful
- Cannot name a case where a decorator is insufficient