Visual walkthrough — Abstract base classes — ABC module, `@abstractmethod`
We assume only three plain ideas, and we build each before using it:
- A class is a template for making objects. Making an object is called instantiation — you write
ClassName()and Python hands you back a new object. - A method is a function that lives inside a class (like
area). - One class can inherit from another (see Inheritance and Method Overriding) — the child gets the parent's methods unless it overrides (rewrites) them.
Everything else — ABC, @abstractmethod, __abstractmethods__, ABCMeta — we define the instant we need it.
Step 1 — The empty blank: what "abstract method" really means
WHAT. We start with a single method that has no real body. We mark it with a special sticker called ==@abstractmethod==. A decorator (the @name written above a function) is just a tag that attaches extra information to that function.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
... # no real code — a blank to be filledWHY this and not a plain method? A normal method does something. We want the opposite: a method that says "I refuse to exist until a subclass writes me." The @abstractmethod sticker is exactly the flag that carries that demand. It writes a hidden mark onto the function object — think of stamping the word BLANK in red on a form field.
PICTURE. Look at the blueprint below. The area box is drawn as an empty amber slot stamped @abstractmethod. It is a hole in the design, not a finished part.

Step 2 — Who reads the stamps? ABC installs an inspector
WHAT. By writing class Shape(ABC) we inherit from ==ABC==. ABC is a tiny helper class whose only job is to attach a special inspector to your class. That inspector is a metaclass called ABCMeta (deep dive: Metaclasses and ABCMeta).
WHY do we need this? A sticker with no reader changes nothing. In Step 1 the @abstractmethod label sits there, but something must walk over the class, collect every stamped method, and refuse construction while any remain. That "something" is the metaclass. Inheriting ABC is how you hire it.
PICTURE. The inspector (cyan magnifying glass) is now clipped to the blueprint. It hasn't reported anything yet — it activates in Step 3.

Step 3 — The inspector writes the tally: __abstractmethods__
WHAT. When the class Shape statement finishes, ABCMeta runs once. It walks every method, collects the names that still carry the @abstractmethod stamp, and stores those names in a hidden ==frozenset (a fixed, unchangeable set) named __abstractmethods__==.
Term by term:
Shape.__abstractmethods__— the tally attached to the class; the double underscores mark it as an internal, Python-managed name.- — "was computed to be".
{'area'}— a set holding one name: the string'area', our still-unfilled blank.
WHY a set of names? Because the rule only needs to know how many blanks remain, not their code. A set answers "is anything left?" in one glance: is it empty or not?
PICTURE. The inspector's clipboard now shows a checklist with one un-ticked box: area.

Step 4 — The construction gate: the rule fires
WHAT. Now someone writes Shape(). Before building anything, ABCMeta reads the tally and applies the central rule:
- — the class you are trying to instantiate.
- — "exactly when", the two sides are true together.
- — the empty set (no blanks left).
For Shape the tally is {'area'}, which is not empty, so the gate slams shut:
Shape()
# TypeError: Can't instantiate abstract class Shape
# with abstract method area
WHY check at construction and not at call? This is the whole payoff. A plain base class that just did raise NotImplementedError would only fail when area() is called — possibly deep inside a loop, hours later. The gate moves the failure forward to the moment of creation, and the message even names the missing blank. Fail early, fail loud.
PICTURE. A red barrier drops across the factory door; the amber area slot is still empty, so the object never leaves.

Step 5 — Filling the blank: a concrete subclass passes the gate
WHAT. A subclass overrides area — it defines a real body, so the stamp is gone on that name.
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2When Circle's class statement finishes, the inspector runs again. It starts from the parent's tally {'area'} and subtracts every name the child has re-defined with real code:
- — set subtraction, "remove from the left set everything in the right set".
- The left set = inherited blanks. The right set = names
Circlefilled with real code. - Result: .
Now the rule from Step 4 gives :
Circle(2).area() # 12.56636 ✅WHY does overriding empty the tally? Overriding means the name now points to a normal (unstamped) function. The inspector, re-scanning, finds no stamp for area, so area is not added to the new tally — it silently disappears. A filled blank is an absent blank.
PICTURE. The amber slot is now a solid cyan part; the checklist box is ticked; the barrier lifts and a finished Circle object rolls out.

Step 6 — The half-finished case: partial override stays blocked
WHAT (degenerate case). What if the parent has two blanks and the child fills only one?
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, amount): ...
@abstractmethod
def refund(self, amount): ...
class CardProcessor(PaymentProcessor):
def pay(self, amount): return f"Charged {amount}"
# refund NOT definedNow trace the tally:
The right set holds only pay (the one name overridden). Subtracting leaves {'refund'} — not empty — so by Step 4's rule, CardProcessor(...) raises TypeError naming refund.
WHY show this? Contracts with many methods are the common case, and this is the trap: "I filled a method, surely I'm concrete now." No — every blank must be filled. The set stays non-empty until the last one is overridden.
PICTURE. Two slots; pay is solid cyan, refund is still an empty amber hole; the barrier is still down.

Step 7 — The edge case that surprises everyone: abstract ≠ empty
WHAT. An abstract method may carry a real body. @abstractmethod forces the override; it does not forbid shared code.
class Repository(ABC):
@abstractmethod
def save(self, item):
print("logging...") # runs when child calls super()
class FileRepo(Repository):
def save(self, item):
super().save(item) # reuse the parent body
# ...then write to fileWHY does the tally not care about the body? The inspector reads only the stamp, never the body. Whether the body is ... or a hundred lines, save is in the parent tally because it is stamped. FileRepo overrides it (removes the stamp) → tally empty → buildable. Yet FileRepo.save can still reach the parent's logic via super().
PICTURE. The save slot is stamped-but-shaded (it holds reusable code), an amber arrow shows the child calling super() back up into it.

The one-picture summary
Every step compressed onto one blueprint: the stamp (Step 1) → the inspector (Step 2) → the tally (Step 3) → the gate rule (Step 4) → subtract on override (Steps 5–6) → body-optional (Step 7).

Recall Feynman: retell the whole walkthrough in plain words
A blueprint (the abstract class) has some blanks drawn in red — those are the @abstractmethod boxes (Step 1). Hire an inspector by writing (ABC) — that inspector is the metaclass (Step 2). The moment the blueprint is finished, the inspector walks it and writes down the list of red blanks still open — that list is __abstractmethods__ (Step 3). Now try to build an object: the factory door checks the list. Any blank left? The door won't open, and it tells you which blank is missing (Step 4). Draw a real part into a blank (override the method) and that blank vanishes from the list; fill the last one and the door swings open (Step 5). Fill only some, the door stays shut on the rest (Step 6). And a "blank" can even come pre-printed with helpful text that a child part copies via super() — filling it just means drawing your own version over it (Step 7). The one rule underneath everything: you can build it exactly when the list of open blanks is empty.
Where this connects
- The override step is Inheritance and Method Overriding doing the erasing of blanks.
- The payoff — a function that trusts
p.pay()exists for any subclass — is Polymorphism. - Contrast the "check names at build time" enforcement here with Duck Typing vs Nominal Typing and the runtime-only style of Protocols (typing.Protocol).
- Other languages bake this into the
interfacekeyword: Interfaces in other languages. - The inspector itself is Metaclasses and ABCMeta.
Recall Trace it yourself
Class A(ABC) stamps f and g. Class B(A) overrides only f. Is B buildable? ::: No — tally is {'f','g'} \ {'f'} = {'g'}, non-empty, so B() raises TypeError naming g.
Which set operation removes a blank when you override a method?
\): the overridden name is subtracted out of the inherited abstract set.Can an abstract method contain real code?
@abstractmethod forces an override but the body may hold shared logic a subclass reuses via super().