2.1.13 · D5OOP Fundamentals

Question bank — Abstract base classes — ABC module, `@abstractmethod`

2,020 words9 min readBack to topic

This bank drills the concepts behind abstract base classes. For related ideas see Inheritance and Method Overriding, Polymorphism, Duck Typing vs Nominal Typing, Metaclasses and ABCMeta, and Protocols (typing.Protocol).


First, a self-contained mental model

Before the traps, let's build the picture the whole page rests on, so you never have to leave this page to reason about it.

Look at the figure below. The left panel shows ABCMeta scanning a class body at definition time and building __abstractmethods__; the right panel shows it intercepting Shape() at call time and comparing that list against the empty set.

Figure — Abstract base classes — ABC module, `@abstractmethod`

The state of __abstractmethods__ at each step — this is the whole story in one table:

Class definition Overrides so far __abstractmethods__ Instantiable?
Shape(ABC) with abstract area none {'area'} ❌ blocked
B(Shape) with pass none {'area'} (inherited) ❌ blocked
Circle(Shape) defines area area frozenset() (empty) ✅ allowed
Card(P) defines pay only pay {'refund'} ❌ blocked
Figure — Abstract base classes — ABC module, `@abstractmethod`

True or false — justify

Decorating a method with @abstractmethod is enough to make its class abstract.
False. The decorator only tags the method (sets __isabstractmethod__); the blocking behaviour comes from the metaclass. Without inheriting ABC (or metaclass=ABCMeta) the tag is silently ignored and the class is fully instantiable.
An abstract base class can never contain any working code.
False. @abstractmethod can wrap a real body (e.g. shared logging), and an ABC can have plenty of ordinary concrete methods too. "Abstract" means "cannot be instantiated," not "empty."
A subclass that overrides every abstract method becomes instantiable.
True. Once every inherited abstract name is redefined in a subclass body, that subclass's __abstractmethods__ becomes the empty set, so the instantiation rule is satisfied.
If a subclass overrides only some of the abstract methods, it is still abstract.
True. The metaclass subtracts only the implemented names from the inherited abstract set; any survivor keeps the set non-empty, so the subclass cannot be built.
Python's ABC machinery checks that an override has the same parameters as the abstract method.
False. It checks only that the name is overridden. Matching signatures is your responsibility (or a static checker like mypy).
You can inherit from ABC even if you declare zero abstract methods.
True, and such a class is instantiable — its __abstractmethods__ is empty from the start. Inheriting ABC alone blocks nothing; the blank blanks do.
__abstractmethods__ is a normal mutable set you can edit to unlock a class.
False. It is a frozenset recomputed by ABCMeta every time a class is defined; you cannot mutate it, and the recompute is driven by what your class body actually overrides.
Re-decorating an inherited method with @abstractmethod in the subclass keeps that subclass abstract.
True. Re-tagging puts the name back into the abstract set, so declaring-without-implementing means the contract is not met and the subclass stays abstract.
An abstract class instance can be created if you never call its abstract methods.
False. The block happens at construction (ClassName() raises TypeError), long before any method call — that is the entire point of moving the error forward.
ABC.register(SomeClass) makes SomeClass a real subclass with all the abstract-method checks.
False. register creates only a virtual subclass: isinstance/issubclass return True, but Python never inspects SomeClass for the abstract methods and never updates its __abstractmethods__. A registered class can be missing every required method and still pass isinstance.

Spot the error

class Shape:
    @abstractmethod
    def area(self): ...
Shape()   # expected to fail?
Why does Shape() succeed here despite @abstractmethod?
Shape does not inherit ABC, so its metaclass is plain type, not ABCMeta. No __abstractmethods__ check runs, the __isabstractmethod__ tag is never consulted, and instantiation is allowed.
class Animal(ABC):
    @abstractmethod
    @property
    def legs(self): ...
What is wrong with this decorator order?
@abstractmethod must be the innermost decorator (closest to def). Here property is applied first, so abstractmethod tags the property object rather than the underlying function, and the abstractness can be lost. Correct order is @property on top, @abstractmethod below.
class Config(ABC):
    @abstractmethod
    @classmethod
    def load(cls): ...
Why is this ordering a trap for @classmethod and @staticmethod too?
The safe rule stays: @abstractmethod must sit directly above def (innermost), with @classmethod/@staticmethod on top of it. Written the other way (as here) abstractmethod tags the classmethod wrapper, and on older Pythons the abstract flag may not propagate — do not assume these wrappers behave identically to a bare function; always put @abstractmethod innermost.
class Repo(ABC):
    @abstractmethod
    def save(self, item): ...
class FileRepo(Repo):
    def save(self): ...     # note: no `item`
FileRepo().save("x")
Does the ABC catch the missing item parameter?
No. The ABC only checks that the name save was overridden, which it was, so FileRepo is concrete. The signature mismatch surfaces only at call time as an ordinary TypeError — ABCs never validate parameters.
class A(ABC):
    @abstractmethod
    def f(self): ...
class B(A):
    pass
B()
Why does B() raise TypeError?
B never overrides f, so f survives in B.__abstractmethods__. The set is non-empty, B is still abstract, and construction is blocked — inheritance alone does not clear the contract.
class P(ABC):
    @abstractmethod
    def pay(self, amt): ...
    @abstractmethod
    def refund(self, amt): ...
class Card(P):
    def pay(self, amt): return amt
Card()
Why is Card still abstract?
Only pay was overridden; refund remains in the inherited abstract set. The metaclass subtracts only implemented names, so __abstractmethods__ is {'refund'} — not empty — and Card() raises TypeError naming refund.

Why questions

Why does an ABC move the error to creation time instead of call time?
A plain base with raise NotImplementedError fails only when the method runs — possibly deep inside a loop, far from the bug's cause. The ABC blocks at construction, so the mistake is caught at the object's birth with a message naming the missing method: fail early, fail loud.
Why must @abstractmethod be the innermost decorator?
abstractmethod works by setting __isabstractmethod__ on the raw function it wraps. It must see the plain function first; if property (or classmethod) wraps the function before it, abstractmethod flags the wrong object and the abstract mark may fail to propagate.
Why can a function type-hinted def checkout(p: PaymentProcessor) skip defensive hasattr checks?
The ABC contract guarantees that any instantiated PaymentProcessor subclass has already overridden every abstract method — so p.pay provably exists. The impossible-to-build objects were rejected before they could ever reach checkout. (Caveat: a register-ed virtual subclass sidesteps that guarantee — see edge cases.)
Why is an ABC called "self-documenting"?
The abstract methods form an explicit, machine-enforced list of exactly what a subclass owes you. Reading the ABC tells you the whole contract, and forgetting any part is impossible rather than merely discouraged.
Why does giving an abstract method a real body not defeat its purpose?
@abstractmethod still forces the override — subclasses cannot be built without redefining the method. The shipped body is reusable shared logic that a subclass opts into via super().method(...); enforcement and code reuse coexist.
Why is __abstractmethods__ recomputed "every time a class is defined"?
Because abstractness depends on the whole inheritance chain: each new class definition must re-check which inherited abstract names it did and did not override. The frozenset is derived, not stored once, so subclasses correctly inherit or clear the contract.

Edge cases

Can you subclass an ABC purely to add new abstract methods (an "extended interface")?
Yes. The subclass simply accumulates its own plus inherited abstract names, all staying in __abstractmethods__. It remains abstract and serves as a richer contract for concrete classes further down.
What happens if a class inherits from two ABCs and overrides all abstract methods from both?
It becomes concrete. The recomputed __abstractmethods__ is the union of both parents' unmet names; overriding everything empties the union, so the rule holds and it instantiates.
Is ABC with no abstract methods equivalent to a normal class for instantiation purposes?
Effectively yes — its __abstractmethods__ is empty so it can be instantiated. The only difference is the ABCMeta machinery lying dormant, ready to enforce abstractness if a subclass later adds @abstractmethod.
Can a concrete subclass be re-made abstract by a further subclass re-tagging a method?
Yes. A grandchild may re-decorate an inherited concrete method with @abstractmethod, putting the name back into its own abstract set and blocking its own instantiation — abstractness is per-class, recomputed at each definition.
Does calling super().__init__() help an abstract class get instantiated?
No. The TypeError is raised by the metaclass before __init__ ever runs. No amount of constructor code can bypass a non-empty __abstractmethods__.
If I use MyABC.register(PlainClass), does PlainClass gain the abstract methods or get checked for them?
No. register only makes isinstance(obj, MyABC) and issubclass(PlainClass, MyABC) report True. It never runs the abstract-method check, never edits PlainClass.__abstractmethods__, and never adds implementations — so a registered class can be silently missing required methods, the classic register gotcha.
Does duck typing bypass the ABC's guarantee entirely?
In a sense — a class that isn't subclassed from (or registered with) the ABC but happens to have the right methods will still "work" where duck typing is used. The ABC's enforcement binds only actual subclasses; see Duck Typing vs Nominal Typing and Protocols (typing.Protocol) for the structural alternative.