Worked examples — Abstract base classes — ABC module, `@abstractmethod`
This deep dive is the drill room for Abstract Base Classes. The parent note built the machinery: ABC, @abstractmethod, and the one rule that governs everything —
Every example below is really one question in disguise: is that set empty or not? We will walk every way the set can stay full, get emptied, or fool you into thinking it is empty.
Before we start, three plain-words reminders so no symbol is unearned:
The scenario matrix
Think of this table like the quadrants of a graph: every real situation you meet lands in exactly one row. If we cover all rows, you can never hit a case we didn't show.
| # | Cell (the "quadrant") | What's special about it | Set empty? → buildable? | Example |
|---|---|---|---|---|
| C1 | All blanks filled | subclass overrides every abstract method | ✅ empty → builds | Ex 1 |
| C2 | One blank left | subclass overrides some, forgets one | ❌ non-empty → blocked | Ex 2 |
| C3 | Zero-blank base | ABC with no abstract methods at all |
✅ empty → builds | Ex 3 |
| C4 | Re-abstracted | subclass re-declares the method abstract | ❌ still non-empty | Ex 4 |
| C5 | Missing metaclass | @abstractmethod but does not inherit ABC |
✅ (decorator ignored!) → builds anyway | Ex 5 |
| C6 | Decorator stacking | abstract @property/@classmethod/@staticmethod — order matters |
depends on order | Ex 6 |
| C7 | Multi-level chain | grandparent → parent → child, blank travels down | tracked across the chain | Ex 7 |
| C8 | Word problem (real) | plugin system: fail-early enforcement | mixture | Ex 8 |
| C9 | Exam twist | abstract body reused via super() |
✅ builds, body reused | Ex 9 |
| C10 | Signature trap | override with wrong parameters | ✅ builds (name only checked!) | Ex 10 |
Two prerequisite ideas power this whole table: Inheritance and Method Overriding (how a name gets replaced) and Metaclasses and ABCMeta (the invisible checker). We link them where they bite.
Figure 1 below is the master diagram for this entire page. It draws the single rule as two mirrored branches. On top, a non-empty set {'refund'} (pink) feeds a pink arrow into a BLOCKED box that raises a TypeError naming the missing method. On the bottom, an empty set frozenset() (blue) feeds a blue arrow into a BUILDS box where ClassName() succeeds. Every example that follows is you deciding which of these two branches the class lands in, so keep glancing back at it: pink arrow = TypeError, blue arrow = an object gets made.

Ex 1 — Cell C1: all blanks filled
Steps. (Trace this on the blue branch of Figure 1.)
- Python defines
Shape. It scans for@abstractmethodtags and findsarea. SoShape.__abstractmethods__ == {"area"}. Why this step? The set is computed at class-definition time, not later — this is what makes errors fire early. - Python defines
Square. It seesSquareoverridesareawith a real body, so it subtracts"area"from the inherited set:Square.__abstractmethods__ == frozenset()(empty). Why this step? Overriding is literally the mechanism that empties the set (see Inheritance and Method Overriding). Square(4)→ set empty → builds..area()returns . Why this step? We are applying the one rule directly: empty__abstractmethods__is the iff-condition for instantiation, so ABCMeta permits the constructor to run. This is the blue branch of Figure 1.
Verify: , prints 16. ✅ (checked in VERIFY)
Ex 2 — Cell C2: one blank left behind
Steps. (This one lands on the pink branch of Figure 1.)
PaymentProcessor.__abstractmethods__ == {"pay", "refund"}— two blanks. Why this step? Each@abstractmethodadds its name to the set.HalfProcessoroverrides onlypay. Python subtracts"pay", leaving{"refund"}. Why this step? Subtraction is name-by-name; unfilled names survive.- Set is non-empty →
HalfProcessor()raisesTypeErrornamingrefund. The actual message reads:
Why this step? This is the rule doing its job — the error moves forward to construction and names the exact missing blank, so you never have to guess which method you forgot.TypeError: Can't instantiate abstract class HalfProcessor with abstract method refund
Verify: sorted(HalfProcessor.__abstractmethods__) == ["refund"]. ✅
Ex 3 — Cell C3: an ABC with zero abstract methods
Steps.
MarkerinheritsABCbut has no@abstractmethodanywhere. SoMarker.__abstractmethods__ == frozenset(). Why this step? InheritingABConly installs the checker (ABCMeta); it does not by itself add blanks.- Set empty → the rule says buildable.
Marker()works,.hello()returns"hi". Why this step? We apply the one rule again: buildability depends only on the set being empty, and nothing here ever put a name in it — so the "surely abstract" intuition fails. Blue branch of Figure 1.
Verify: Marker().hello() == "hi" and the set is empty. ✅
Ex 4 — Cell C4: re-abstracting keeps it blank
Steps.
A.__abstractmethods__ == {"f"}. Why this step? The single@abstractmethodonfis the only tag, so exactly one name enters the set — this is our starting point beforeBeven exists.Bre-declaresfbut tags it@abstractmethodagain — so it is still a blank, not a fill. Why this step? Only a real body (an override without the tag) removes the name. Re-tagging re-adds it.B.__abstractmethods__ == {"f"}→ non-empty →B()raisesTypeError. Why this step? Applying the one rule: the set never emptied (step 2 re-inserted"f"), so the iff-condition for instantiation fails and ABCMeta blocks the constructor. Pink branch of Figure 1.
Verify: "f" in B.__abstractmethods__ is True. ✅
Ex 5 — Cell C5: the missing-metaclass trap
Steps.
Plaindoes not inheritABC, so its metaclass is the ordinarytype, not ABCMeta. Why this step? The whole enforcement lives in ABCMeta. No ABCMeta → no checker → no__abstractmethods__gathering.@abstractmethodmerely sets a hidden flagf.__isabstractmethod__ = True. With no ABCMeta to read that flag, nothing happens. Why this tool? We inspect__isabstractmethod__to prove the flag was set yet ignored.Plain()builds happily. The decorator was silently useless. Why this step? The one rule can only be enforced by ABCMeta; with the plaintypemetaclass there is no__abstractmethods__to consult, so construction is never gated and the object is made regardless of the flag. This is why C5 sits on the blue (builds) branch despite looking abstract.
Verify: Plain.f.__isabstractmethod__ is True and Plain() builds (no exception). ✅
Ex 6 — Cell C6: stacking decorators — order is the whole game
Three built-in decorators commonly sit on top of @abstractmethod: @property, @classmethod, and @staticmethod. The rule is always the same and worth stating once, plainly:
Steps.
- In every trio,
@abstractmethodis placed closest todef, so it tags the raw underlying function first; then@property/@classmethod/@staticmethodwraps that already-tagged function. All three names therefore enter the abstract set:Animal.__abstractmethods__ == {"legs","species","kingdom"}. Why this order? Decorators apply bottom-up (innermost first). Only the function object itself is what ABCMeta inspects for the__isabstractmethod__flag; if you flipped the order,property/classmethod/staticmethodwould be tagged instead of the function, and the abstract flag could be silently lost — exactly the parent note's ordering mistake. Animalhas three names in its set → non-empty →Animal()is blocked. Why this step? Direct application of the one rule: a non-empty set is the iff-condition for a blocked constructor (pink branch, Figure 1).Dogoverrides all three with concrete versions (no abstract tags), so ABCMeta subtracts all three names → set empties →Dog()builds. Why this step? Each concrete override without the tag is what removes a name; three removals from a three-element set leavesfrozenset(), satisfying the one rule (blue branch).- Reading them:
legsis a property → read asDog().legswithout parentheses;speciesis a classmethod andkingdoma staticmethod → both are called with parentheses (Dog.species(),Dog.kingdom()). Why this step? The wrapping decorator determines the access protocol: a property exposes a value on attribute access, whereas class/static methods stay callables. Abstractness never changed that — it only decided whether you were forced to supply them.
Verify: Dog().legs == 4, Dog.species() == "canis", Dog.kingdom() == "animalia", and Animal.legs.fget.__isabstractmethod__ is True. ✅
Ex 7 — Cell C7: the blank travels down a 3-level chain
Steps. (Figure 2 draws this exact chain — watch the set shrink row by row.)
Base.__abstractmethods__ == {"a","b"}. Why this step? Two@abstractmethodtags mean two names enter the set; this is the full set of blanks the whole family inherits, and it is the top row of Figure 2.Midfillsa, leaving{"b"}→Midis still abstract, cannot be built. Why this step? Applying the one rule:Midoverrode onlya, so ABCMeta subtracts just"a"; the surviving{"b"}is non-empty, so the iff-condition blocksMid(). Partial fills accumulate down the chain — the middle (pink) row of Figure 2.Leafinherits{"b"}fromMid, fillsb→ set empties →Leaf()builds. Why this step? The abstract set is recomputed at each class definition from what remains unimplemented;Leafstarts fromMid's leftover{"b"}and removes"b", givingfrozenset(), so the one rule now permits construction — the bottom (blue) row of Figure 2.Leaf().a()uses the body inherited fromMid("a"),Leaf().b()returns"b", so the print showsa b. Why this step? Method resolution walks up the inheritance chain:Leafhas noaof its own, so Python findsMid.a(standard override/inheritance lookup);bis found directly onLeaf. This confirms that even thoughMiditself could never be built, its concreteabody is still inherited and usable by the concreteLeaf.
Verify: Mid.__abstractmethods__ == {"b"}, Leaf.__abstractmethods__ == frozenset(), (Leaf().a(), Leaf().b()) == ("a","b"). ✅
Figure 2 makes step 2's "partial fill" visible: the abstract set on the right shrinks from {a, b} to {b} to { } as we descend, and only the bottom (empty) row is coloured blue for "builds".

Ex 8 — Cell C8: real-world plugin loader (word problem)
Steps.
Exporter.__abstractmethods__ == {"export","name"}— two obligations (this is the polymorphic contract every plugin must honour). Why this step? Two@abstractmethodtags (one plain, one under@property) put both names in the set, defining the contract every subclass owes.CsvExporterfills both → buildable.load(CsvExporter)succeeds. Why this step? Applying the one rule: both overrides are tagless concrete definitions, so both names are subtracted, leavingfrozenset()and permitting construction insideload.export([1,2,3])→"csv:3 rows"sincelen([1,2,3]) == 3andname == "csv". Why this step? We plug the concrete values back into the f-string body to get the actual returned text — a direct sanity substitution.BrokenExporterfills onlyexport;{"name"}survives →load(BrokenExporter)raisesTypeErrorat thecls()line. Why this step? The one rule again: the leftover{"name"}is non-empty, so the failure fires at thecls()construction call — exactly the fail-early behaviour we designed for.
Verify: load(CsvExporter).export([1,2,3]) == "csv:3 rows", and "name" in BrokenExporter.__abstractmethods__. ✅
Ex 9 — Cell C9: abstract method with a reusable body
Steps.
Repo.__abstractmethods__ == {"save"}— the tag forces an override even though a body exists. Why this step? The abstract flag is about obligation, not about emptiness. A body is allowed and callable viasuper().FileRepooverridessave(empties the set → buildable) and callssuper().save("x")→"log:x". Why this step? The override is tagless, so"save"is subtracted and the one rule permitsFileRepo();super()still reaches the parent's real body because the body was never deleted — being abstract does not blank it out.- It appends
";written"→"log:x;written". Why this step? Direct substitution ofbase = "log:x"intobase + ";written"gives the returned string — a plug-back sanity check.
Verify: FileRepo().save("x") == "log:x;written". ✅
Ex 10 — Cell C10: the signature trap
Steps.
Src.__abstractmethods__ == {"read"}. Why this step? One@abstractmethodtag → one name in the set; this is the sole obligationWeirdmust clear.Weirddefines a method literally namedread. Python subtracts the name"read"— it never inspects the parameter list. Why this step? The rule keys on names only. Signature checking is not part of ABCs.- Set empties →
Weird()builds, andWeird().read()returns"data". Why this step? Applying the one rule: the name"read"was removed, sofrozenset()permits construction — meaning the parameter mismatch is a latent bug the ABC cannot see. A static checker like mypy or typing.Protocol would catch it; the ABC will not.
Verify: Weird().read() == "data" and Weird.__abstractmethods__ == frozenset(). ✅
The whole matrix, at a glance
Recall One-line summary of all ten cells
Buildable exactly when __abstractmethods__ is empty; it empties only by a real, un-tagged override of every name, the checker only exists if you inherit ABC/ABCMeta, and it checks names, never signatures.
Buildable when?
__abstractmethods__ is completely empty.What empties a name from the set?
Does re-tagging @abstractmethod fill the blank?
Why is @abstractmethod ignored without ABC?
Does an ABC check parameter signatures?
Is an ABC with zero abstract methods buildable?
Where must @abstractmethod sit under @classmethod/@staticmethod/@property?
def — so the flag lands on the function itself.