2.1.13 · D3OOP Fundamentals

Worked examples — Abstract base classes — ABC module, `@abstractmethod`

3,447 words16 min readBack to topic

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.

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

Ex 1 — Cell C1: all blanks filled

Steps. (Trace this on the blue branch of Figure 1.)

  1. Python defines Shape. It scans for @abstractmethod tags and finds area. So Shape.__abstractmethods__ == {"area"}. Why this step? The set is computed at class-definition time, not later — this is what makes errors fire early.
  2. Python defines Square. It sees Square overrides area with 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).
  3. 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.)

  1. PaymentProcessor.__abstractmethods__ == {"pay", "refund"} — two blanks. Why this step? Each @abstractmethod adds its name to the set.
  2. HalfProcessor overrides only pay. Python subtracts "pay", leaving {"refund"}. Why this step? Subtraction is name-by-name; unfilled names survive.
  3. Set is non-emptyHalfProcessor() raises TypeError naming refund. The actual message reads:
    TypeError: Can't instantiate abstract class HalfProcessor
    with abstract method refund
    
    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.

Verify: sorted(HalfProcessor.__abstractmethods__) == ["refund"]. ✅


Ex 3 — Cell C3: an ABC with zero abstract methods

Steps.

  1. Marker inherits ABC but has no @abstractmethod anywhere. So Marker.__abstractmethods__ == frozenset(). Why this step? Inheriting ABC only installs the checker (ABCMeta); it does not by itself add blanks.
  2. 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.

  1. A.__abstractmethods__ == {"f"}. Why this step? The single @abstractmethod on f is the only tag, so exactly one name enters the set — this is our starting point before B even exists.
  2. B re-declares f but tags it @abstractmethod again — 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.
  3. B.__abstractmethods__ == {"f"} → non-empty → B() raises TypeError. 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.

  1. Plain does not inherit ABC, so its metaclass is the ordinary type, not ABCMeta. Why this step? The whole enforcement lives in ABCMeta. No ABCMeta → no checker → no __abstractmethods__ gathering.
  2. @abstractmethod merely sets a hidden flag f.__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.
  3. Plain() builds happily. The decorator was silently useless. Why this step? The one rule can only be enforced by ABCMeta; with the plain type metaclass 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.

  1. In every trio, @abstractmethod is placed closest to def, so it tags the raw underlying function first; then @property / @classmethod / @staticmethod wraps 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/staticmethod would be tagged instead of the function, and the abstract flag could be silently lost — exactly the parent note's ordering mistake.
  2. Animal has 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).
  3. Dog overrides 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 leaves frozenset(), satisfying the one rule (blue branch).
  4. Reading them: legs is a property → read as Dog().legs without parentheses; species is a classmethod and kingdom a 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.)

  1. Base.__abstractmethods__ == {"a","b"}. Why this step? Two @abstractmethod tags 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.
  2. Mid fills a, leaving {"b"}Mid is still abstract, cannot be built. Why this step? Applying the one rule: Mid overrode only a, so ABCMeta subtracts just "a"; the surviving {"b"} is non-empty, so the iff-condition blocks Mid(). Partial fills accumulate down the chain — the middle (pink) row of Figure 2.
  3. Leaf inherits {"b"} from Mid, fills b → set empties → Leaf() builds. Why this step? The abstract set is recomputed at each class definition from what remains unimplemented; Leaf starts from Mid's leftover {"b"} and removes "b", giving frozenset(), so the one rule now permits construction — the bottom (blue) row of Figure 2.
  4. Leaf().a() uses the body inherited from Mid ("a"), Leaf().b() returns "b", so the print shows a b. Why this step? Method resolution walks up the inheritance chain: Leaf has no a of its own, so Python finds Mid.a (standard override/inheritance lookup); b is found directly on Leaf. This confirms that even though Mid itself could never be built, its concrete a body is still inherited and usable by the concrete Leaf.

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

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

Ex 8 — Cell C8: real-world plugin loader (word problem)

Steps.

  1. Exporter.__abstractmethods__ == {"export","name"} — two obligations (this is the polymorphic contract every plugin must honour). Why this step? Two @abstractmethod tags (one plain, one under @property) put both names in the set, defining the contract every subclass owes.
  2. CsvExporter fills both → buildable. load(CsvExporter) succeeds. Why this step? Applying the one rule: both overrides are tagless concrete definitions, so both names are subtracted, leaving frozenset() and permitting construction inside load.
  3. export([1,2,3])"csv:3 rows" since len([1,2,3]) == 3 and name == "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.
  4. BrokenExporter fills only export; {"name"} survives → load(BrokenExporter) raises TypeError at the cls() line. Why this step? The one rule again: the leftover {"name"} is non-empty, so the failure fires at the cls() 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.

  1. 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 via super().
  2. FileRepo overrides save (empties the set → buildable) and calls super().save("x")"log:x". Why this step? The override is tagless, so "save" is subtracted and the one rule permits FileRepo(); super() still reaches the parent's real body because the body was never deleted — being abstract does not blank it out.
  3. It appends ";written""log:x;written". Why this step? Direct substitution of base = "log:x" into base + ";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.

  1. Src.__abstractmethods__ == {"read"}. Why this step? One @abstractmethod tag → one name in the set; this is the sole obligation Weird must clear.
  2. Weird defines a method literally named read. 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.
  3. Set empties → Weird() builds, and Weird().read() returns "data". Why this step? Applying the one rule: the name "read" was removed, so frozenset() 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

yes

no

removes name

keeps name

checker absent

Is set of abstract methods empty?

Can instantiate

TypeError names missing method

Override with real body

Re-tag abstractmethod

No ABC no ABCMeta

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?
Only when __abstractmethods__ is completely empty.
What empties a name from the set?
A concrete override (real body, no abstract tag) in a subclass.
Does re-tagging @abstractmethod fill the blank?
No — the name stays; the subclass stays abstract.
Why is @abstractmethod ignored without ABC?
No ABCMeta metaclass means no checker reads the abstract flag.
Does an ABC check parameter signatures?
No — only that the method name is overridden.
Is an ABC with zero abstract methods buildable?
Yes — empty set → instantiable.
Where must @abstractmethod sit under @classmethod/@staticmethod/@property?
Innermost — directly above def — so the flag lands on the function itself.