skip to content

Metaclasses, ABCs and Class Machinery

The layer beneath the class statement: classes are objects made by a metaclass, ABCs turn a missing method into an instantiation error, and __slots__ changes how instances store attributes. The ceiling question.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

12

Why does instantiating a subclass of abc.ABC that leaves an @abstractmethod unimplemented raise TypeError?

level: juniorimportance: must knowfreq 65%

answer

  1. Enforcement lives in the metaclass
  2. A frozenset computed at class creation
  3. Construction refuses, definition succeeds
  4. __abstractmethods__ must be empty to instantiate
  5. The decorator only sets a flag

basics

~20 s

abc.ABCMeta collects every still-abstract name into a frozenset stored on the class as abstractmethods, and object construction refuses to run while that set is non-empty. The failure therefore lands at instantiation, not at class definition.

solid answer

~40 s

`@abc.abstractmethod` only sets `__isabstractmethod__ = True` on the function; the enforcement is in the metaclass. When `ABCMeta` builds a class it collects the names that are still abstract — from the namespace and from each base's set — into `__abstractmethods__`, and `object.__new__` refuses to construct an instance while that frozenset is non-empty, raising `TypeError: Can't instantiate abstract class PartialImporter without an implementation for abstract method 'rollback'`. Two consequences matter in review. Defining the incomplete subclass **succeeds**: the module imports cleanly and only the call fails, so a missing method is a first-construction runtime error. And the check is name-based, not signature-based, so binding the name to anything — even `load = None` — clears it. On a class that does not go through `ABCMeta`, `@abstractmethod` is inert decoration and instances are created happily.

code

python · 19 lines
python
from abc import ABC, abstractmethod

class CatalogueImporter(ABC):
    @abstractmethod
    def load(self, record): ...

    @abstractmethod
    def rollback(self): ...

class PartialImporter(CatalogueImporter):
    def load(self, record):
        return record

print(PartialImporter.__abstractmethods__)

try:
    PartialImporter()
except TypeError as exc:
    print(exc)

go deeper

for a junior

Be ready to recall the sequence in one breath: decorate with @abstractmethod, inherit from abc.ABC, and the TypeError appears when someone tries to create the object. Knowing that the class definition itself succeeds is the part that separates a memorised answer from an understood one.

for a middle

Explain the mechanics: the decorator sets a flag on the function, ABCMeta gathers the still-flagged names into abstractmethods at class creation, and object.new checks that set. Mention that the check is name presence only, so signatures are never verified.

for a senior

Show you have debugged it in production. The failure is a first-construction runtime error, not an import error, so it can survive a deploy and surface on a rare code path; and an ABC guarantees only that a name exists, which is why review, not the machinery, catches wrong signatures.

for a principal

Own the question of whether nominal ABCs are the right contract mechanism for your code base at all, given that they enforce names rather than behaviour and add a metaclass to every subclass. Be able to say when a plain convention plus static checking buys more than runtime enforcement.

### The decorator does almost nothing `abc.abstractmethod` is three lines of Python. It sets `__isabstractmethod__ = True` on the function object handed to it and returns that same function unchanged. It does not wrap the function, does not replace its body, and does not block anything by itself. An abstract method is an ordinary method carrying a flag; its body still runs if a subclass calls it through `super()`, which is why abstract methods in real code are often a useful default rather than `...`. ### The metaclass reads the flag The enforcement lives in `abc.ABCMeta`. Writing `class CatalogueImporter(ABC)` is simply the readable way to get that metaclass — `class CatalogueImporter(metaclass=ABCMeta)` builds exactly the same thing. When `ABCMeta` creates a class it does two passes. First it scans the new class namespace for names whose value has a truthy `__isabstractmethod__`. Then, for every base class that already has an `__abstractmethods__` set, it re-resolves each of those names with `getattr` on the new class and keeps the ones that are *still* abstract. The union is frozen and stored as a class attribute, `__abstractmethods__`. So `PartialImporter.__abstractmethods__` is `frozenset({'rollback'})`: `load` was resolved to a concrete function defined in the subclass and dropped out of the set, `rollback` resolved back to the flagged function on the base and stayed. ### Where the TypeError is raised `object.__new__` refuses to build an instance of a class whose `__abstractmethods__` is non-empty, and that is the whole enforcement: ``` TypeError: Can't instantiate abstract class PartialImporter without an implementation for abstract method 'rollback' ``` With several missing names the message pluralises and lists them all: `... without an implementation for abstract methods 'load', 'rollback'`. The timing is the part interviews probe. **Class creation succeeds.** The module imports cleanly, the incomplete class is a perfectly good object, you can subclass it further, and static analysis or an editor may flag nothing. Only the call `PartialImporter()` fails. A missing implementation is therefore a runtime error on the first construction — which in a service means the first request that reaches that code path, not startup, unless something constructs the object eagerly at import. ### The check is name presence, not a contract `ABCMeta` asks only whether the name resolves to something non-abstract. It never inspects a signature, a return type or a docstring. Consequences worth saying out loud: * `def load(self): ...` in the subclass satisfies the base's `load(self, record)` even though calling it will fail with a `TypeError` about arguments. * Binding the name to a non-function clears it too: `load = None` in the class body makes the class instantiable, and so does a class-level attribute or an assignment to a `lambda`. * An ABC with no abstract methods at all is instantiable. `class Marker(ABC): pass` builds instances happily; `ABC` itself is only "abstract" in the sense that it supplies the metaclass. * If a class decorator adds or removes methods *after* the class object exists, the frozenset is already computed and stale; `abc.update_abstractmethods` recomputes it. Structural checkers do enforce signatures, but that is a separate, static story — the ABC machinery at runtime is presence-only. ### The failure mode when ABCMeta is absent Decorating methods with `@abstractmethod` on a class that is **not** built by `ABCMeta` does nothing enforceable. There is no `__abstractmethods__` attribute, `object.__new__` has nothing to check, and instances are created without complaint. This is the most common way an intended interface silently stops being one: someone refactors the base class, drops the `ABC` base (or introduces a competing metaclass) and every `@abstractmethod` decoration downgrades to documentation. ### What to say in an interview State the three moving parts in order: the decorator sets a flag on the function; `ABCMeta` collects the still-flagged names into `__abstractmethods__` at class-creation time; `object.__new__` raises `TypeError` while that set is non-empty. Then add the two facts that show you have debugged it — the error is at instantiation, not definition, and the check is name-based, so an ABC guarantees that a name exists and nothing about what it accepts or returns. If you want a compile-time contract on argument types, an ABC is not the tool; if you want "this object cannot be built until someone supplies a rollback", it is exactly the tool.

  • Does the class definition itself fail, or only the call that creates the instance?
    Only the call. `ABCMeta` records the outstanding names in `__abstractmethods__` while creating the class and raises nothing; the class object is perfectly usable and can be subclassed further. `object.__new__` is what refuses, so the `TypeError` appears on the first construction — in a service that is the first request reaching that path, not startup.
  • How can a subclass clear the check without writing a real implementation?
    By binding the name to anything non-abstract. `load = None` in the class body, a class-level attribute, or a one-line `lambda` all satisfy `ABCMeta`, which only asks whether the name still resolves to something flagged abstract. Signatures are never inspected. If a class decorator injects methods after the class exists, the frozenset is stale and `abc.update_abstractmethods` recomputes it.
  • What does @abstractmethod do on a class whose metaclass is not ABCMeta?
    Nothing enforceable. It sets `__isabstractmethod__` on the function and stops there; without `ABCMeta` there is no `__abstractmethods__` attribute and `object.__new__` has nothing to check, so instances are created normally. This is how an intended interface quietly stops being one after someone drops the `abc.ABC` base during a refactor.

The decorator is a sticky note on a method; ABCMeta is the clerk who counts the notes when the class is filed, and object construction is the door that stays shut while any note is left.

saying these in an interview costs you the question

  • Says the TypeError is raised when the class is defined
  • Thinks @abstractmethod blocks instantiation without ABCMeta
  • Believes Python checks the abstract method's signature
  • Claims an ABC with no abstract methods cannot be instantiated
  • Treats abc.ABC as a keyword rather than a base supplying a metaclass

context

open as a page

What does defining `__slots__` on a Python class change about its instances?

level: juniorimportance: must knowfreq 45%

basics

~10 s

Instances lose their per-instance __dict__. Only the names listed in __slots__ can be assigned, each into a fixed storage slot, and assigning any other attribute raises AttributeError instead of silently creating it.

open as a page

What does the three-argument `type(name, bases, namespace)` call create in Python?

level: middleimportance: must knowfreq 55%

basics

~10 s

It creates and returns a brand-new class object at runtime. type('Duck', (Bird,), {'legs': 2}) produces the same kind of object a class Duck(Bird) statement would. The one-argument form, type(obj), instead reports an object's class.

open as a page

How must @abc.abstractmethod be stacked with @property or @classmethod, and why?

level: middleimportance: should knowfreq 40%

basics

~10 s

Put @abstractmethod innermost, directly above the def, with @property, @classmethod or @staticmethod outside it. The reverse order raises AttributeError while the class body runs, because those wrapper objects expose a read-only isabstractmethod.

open as a page

When a class uses `metaclass=Meta`, in what order do `__prepare__`, `__new__` and `__init__` run?

level: middleimportance: should knowfreq 40%

basics

~20 s

Meta.__prepare__ runs first and returns the mapping the class body executes into. The body then runs, and the filled mapping is handed to Meta.__new__, which creates the class object, and finally to Meta.__init__, which configures it.

open as a page

How does a class declaring `__slots__` store attribute values with no instance `__dict__`?

level: middleimportance: should knowfreq 32%

basics

~10 s

Each name in __slots__ becomes a class-level descriptor bound to a fixed storage cell in the instance's memory layout. Attribute access runs that descriptor's get, set and delete hooks instead of any dictionary lookup.

open as a page

What does ABCMeta.register give a virtual subclass, and what does it never provide?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Registration makes isinstance and issubclass answer True for a class that never inherited. It changes nothing else: the registered class keeps its own MRO, inherits no methods or defaults from the ABC, and its abstract methods are never verified.

open as a page

A row class declares `__slots__`, yet its instances still accept a misspelled attribute. Why?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Some class in the MRO does not declare __slots__, so it contributes an instance __dict__ that every subclass inherits. The declaration constrains only the class that writes it; one slot-less ancestor or subclass reopens the object.

open as a page

How do you decide between a metaclass and a class decorator for enforcing a rule across many classes?

level: principalimportance: should knowfreq 35%

basics

~20 s

Decide 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.

open as a page

Why is isinstance(obj, collections.abc.Iterable) True for a class that never subclassed or registered with it?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

collections.abc.Iterable defines subclasshook, a classmethod that ABCMeta consults during every issubclass check. It looks for iter anywhere in the candidate class's MRO and answers True, so no inheritance and no registration are needed.

open as a page

What does a metaclass's `__call__` control that a class's own `__new__` cannot?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

It controls the whole act of calling the class: Duck() runs type(Duck).__call__, so a metaclass can return a cached object without __new__ or __init__ running at all. A cached instance returned from __new__ still gets __init__ re-run.

open as a page

Why does `weakref.ref()` raise TypeError on an instance of a class declaring `__slots__`?

level: seniorimportance: nice to knowfreq 15%

basics

~10 s

Weak-reference support needs its own storage cell, which a slotted class only gets if "__weakref__" is listed in the declaration. Without it the type cannot hold weak references and weakref.ref() refuses the object.

open as a page