Exercises — Abstract base classes — ABC module, `@abstractmethod`
First, two pieces of notation this page leans on
Before any exercise, let's un-magic the two symbols in the central rule so nothing below surprises you.
(1) The special attribute __abstractmethods__. Every class in Python secretly carries labelled boxes of information ("attributes"). When a class inherits ABC, the machinery quietly attaches one extra box named __abstractmethods__ — a frozenset (an unchangeable bag of names) listing every abstract method that has NOT yet been filled in. You can print it: print(Shape.__abstractmethods__). The double underscores just signal "this is Python-internal book-keeping, not something you set by hand." Think of it as the checklist of remaining blanks stapled to the class.
(2) The two maths symbols in the rule.
can_instantiate(C)is just function notation: it means "the yes/no answer to the question — can we build an object of classC?" Readcan_instantiate(C)as "isCbuildable?" —Cis the input, and the output isTrueorFalse.∅(or\varnothing) is the empty-set symbol: a bag with nothing in it. So__abstractmethods__ = ∅means "the checklist of remaining blanks is empty — every blank is filled."⟺reads "if and only if": the left side is true exactly when the right side is true, in both directions.
Now the annotated picture of that checklist box and the buildable/blocked decision it drives:

C carries a stapled checklist box __abstractmethods__. The gate reads that box: if the box is empty (∅, green path) the constructor C() builds a concrete object; if the box still holds any name (red path) C() raises TypeError and prints exactly which blank is missing. This single gate is every exercise on the page.
L1 — Recognition
Goal: recognise what makes a class abstract and what happens when you touch it.
Exercise 1.1
Which of these classes is abstract (cannot be instantiated)? Explain each.
from abc import ABC, abstractmethod
# (a)
class A:
@abstractmethod
def f(self): ...
# (b)
class B(ABC):
@abstractmethod
def f(self): ...
# (c)
class C(ABC):
def f(self): ...Recall Solution 1.1
- (a) NOT abstract.
Adecoratesfwith@abstractmethodbut ==does not inheritABC==. The decorator only tags the function; the blocking is done by the metaclassABCMeta, which arrives withABC. SinceA's metaclass is the ordinarytype, nobody ever reads the tag (and no__abstractmethods__checklist is even stapled on).A()succeeds. - (b) ABSTRACT.
BinheritsABCand has an@abstractmethod. SoB.__abstractmethods__ = frozenset({'f'}), which is non-empty →B()raisesTypeError. - (c) NOT abstract.
CinheritsABCbut has zero abstract methods (fis a normal method).C.__abstractmethods__ = frozenset()(empty, i.e.∅) →C()works fine.
Rule check: abstract requires both ingredients — ABC (the enforcer) and at least one live @abstractmethod (something to enforce).
Exercise 1.2
True or false: from abc import ABC is enough to make any class that inherits it un-instantiable.
Recall Solution 1.2
False. Inheriting ABC only enables the check. If the class has no abstract methods, __abstractmethods__ is empty and it instantiates normally (see 1.1c). ABC is the referee; you still need at least one @abstractmethod for there to be a foul.
L2 — Application
Goal: correctly write and instantiate concrete subclasses.
Exercise 2.1
Given:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): ...Write a concrete Rectangle with a width w and height h, then compute the area of a 4 × 5 rectangle.
Recall Solution 2.1
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
Rectangle(4, 5).area() # 20Why it works: Rectangle overrides area (redefines it in its own body). The metaclass subtracts area from the inherited checklist → Rectangle.__abstractmethods__ is empty (∅) → instantiation allowed. Answer: .
Exercise 2.2
Given the Shape above, add a Circle(r) and compute the area of a circle with radius 3 using .
Recall Solution 2.2
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14159 * self.r ** 2
Circle(3).area() # 28.27431Why: override area → contract cleared. .
Exercise 2.3
A subclass overrides area but also re-decorates it with @abstractmethod:
class Blob(Shape):
@abstractmethod
def area(self): return 0Can you write Blob()? Why or why not?
Recall Solution 2.3
No — Blob() raises TypeError. Re-tagging area with @abstractmethod puts it back into Blob.__abstractmethods__. Even though it has a body (return 0), the presence of the tag means "this is still a blank to be filled by my subclasses." Blob is therefore abstract again. A body does not cancel abstractness — only removing the tag on override does.
L3 — Analysis
Goal: trace the __abstractmethods__ set through an inheritance chain.
Exercise 3.1
Predict the output. Then explain each __abstractmethods__ set.
from abc import ABC, abstractmethod
class A(ABC):
@abstractmethod
def f(self): ...
@abstractmethod
def g(self): ...
class B(A):
def f(self): return "f"
class C(B):
def g(self): return "g"
print(A.__abstractmethods__)
print(B.__abstractmethods__)
print(C.__abstractmethods__)
try:
C()
print("C built")
except TypeError:
print("C blocked")Recall Solution 3.1
The checklist is recomputed at each class definition: start from the parent's checklist, remove any name this class overrides with a non-abstract method, add any new @abstractmethod.
A.__abstractmethods__=frozenset({'f', 'g'})— both declared abstract, neither implemented.B.__abstractmethods__=frozenset({'g'})—Boverridesf(removesf);gis still inherited-but-unimplemented, so it survives.Bis still abstract.C.__abstractmethods__=frozenset()(∅) —Coverridesg, andfwas already cleared byB. Empty set.- So
C()→ prints "C built".
Now trace it in the figure below: watch the checklist shrink one name at a time as we descend, and see the exact level where it first becomes empty (∅) and the gate finally opens.

A → B → C. At A the checklist holds {f, g} (red, blocked). At B we override f, so the checklist shrinks to {g} — still non-empty, still blocked (orange). At C we override g, the checklist becomes ∅ (green), and only here does can_instantiate(C) flip to True. The gate on the right opens exactly at the empty box, which is the L1 figure's rule applied three times in a row.
Exercise 3.2
In Ex 3.1, is B instantiable? If you call B(), what error message names which method?
Recall Solution 3.2
B is NOT instantiable. B.__abstractmethods__ = {'g'} is non-empty. B() raises:
TypeError: Can't instantiate abstract class B with abstract method g.
The message names exactly the surviving method (g) — this is the "fail loud, name the blank" behaviour the whole ABC machinery buys you.
L4 — Synthesis
Goal: design your own ABCs, including abstract properties, class/static methods, and shared bodies.
Exercise 4.1
Design an ABC Employee with:
- an abstract property
salary(subclasses must provide it), - an abstract method
role(self).
Then write a concrete Manager whose salary is 90000 and whose role returns "manager". Instantiate it and read both.
Recall Solution 4.1
from abc import ABC, abstractmethod
class Employee(ABC):
@property
@abstractmethod
def salary(self): ...
@abstractmethod
def role(self): ...
class Manager(Employee):
@property
def salary(self):
return 90000
def role(self):
return "manager"
m = Manager()
m.salary # 90000
m.role() # "manager"Why the decorator order? @property must be outermost (top), @abstractmethod innermost (touching def). Reason: @abstractmethod needs to tag the raw function before property wraps it. If the tag sat on the outside, property — not your method — would be marked abstract, and the abstractness could leak away. This ordering is the same idea used in Polymorphism where the interface shape matters more than the storage.
Exercise 4.2
The same innermost-tag rule applies to @classmethod and @staticmethod. Write an ABC Factory with an abstract class method build(cls) and an abstract static method spec(). Then a concrete WidgetFactory that implements both. What is the correct decorator order, and what breaks with the wrong order?
Recall Solution 4.2
from abc import ABC, abstractmethod
class Factory(ABC):
@classmethod
@abstractmethod
def build(cls): ...
@staticmethod
@abstractmethod
def spec(): ...
class WidgetFactory(Factory):
@classmethod
def build(cls):
return "widget"
@staticmethod
def spec():
return "spec-v1"
WidgetFactory.build() # "widget"
WidgetFactory.spec() # "spec-v1"Correct order: @classmethod (or @staticmethod) outermost/top, @abstractmethod innermost/bottom, touching def — exactly like @property. The reason is identical: @abstractmethod must tag the raw underlying function first, and only then may classmethod/staticmethod wrap that tagged function.
What breaks with the wrong order (@abstractmethod on top of @classmethod): the abstract tag would land on the already-wrapped classmethod object rather than the raw function, so the name may not land in __abstractmethods__ — the abstractness silently disappears and a subclass forgetting build would slip through the gate. (Modern CPython special-cases some of this, but the safe, portable rule is always: @abstractmethod innermost.)
Exercise 4.3
Build an ABC Logger with an abstract method write(self, msg) that ships a shared body printing "[log]". Then a concrete FileLogger that reuses the parent body via super() and appends " -> file". Show what FileLogger().write("hi") returns.
Recall Solution 4.3
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def write(self, msg):
return f"[log] {msg}"
class FileLogger(Logger):
def write(self, msg):
base = super().write(msg) # reuse abstract method's body
return base + " -> file"
FileLogger().write("hi") # "[log] hi -> file"Why this is legal: @abstractmethod forces the override but does not forbid a body. FileLogger overrides write (clearing the checklist → instantiable), then chooses to call the parent's body with super(). Abstract ≠ empty. Result: "[log] hi -> file".
L5 — Mastery
Goal: reason at the edges — subtle, degenerate, and cross-mechanism cases.
Exercise 5.1 — The signature trap
from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def send(self, msg, urgent): ...
class Impl(Base):
def send(self): # WRONG number of params
return "sent"
Impl() # does this raise?Does Impl() succeed? What does this reveal about what ABCs check?
Recall Solution 5.1
Impl() SUCCEEDS. Python's ABC machinery checks only that the name send is overridden with a non-abstract method — it does not compare parameter lists. Impl.__abstractmethods__ is empty (∅), so the object builds. The mismatch only explodes later, when someone calls impl.send("hi", True) and gets a TypeError: send() takes 1 positional argument but 3 were given.
Reveal: ABCs enforce presence of a name, not signature compatibility. For signature checking you need a static type checker (mypy) or Protocols (typing.Protocol).
Exercise 5.2 — Zero abstract methods, degenerate case
from abc import ABC
class Empty(ABC):
pass
Empty() # ?Does this build? Connect your answer to the one rule.
Recall Solution 5.2
It builds. Empty inherits ABC but declares no abstract methods, so Empty.__abstractmethods__ = frozenset() (empty, ∅). By the rule , empty checklist → instantiable. Inheriting ABC is necessary but not sufficient for abstractness; you also need a live blank.
Exercise 5.3 — Duck typing vs ABC enforcement
An object that is not a subclass of PaymentProcessor but has a pay method is passed to:
def checkout(p, amt):
return p.pay(amt)Will checkout run? Contrast this with what the ABC guarantees.
Recall Solution 5.3
checkout runs fine — at call time Python only needs the object to have a callable pay (Duck Typing vs Nominal Typing: "if it quacks, it's a duck"). The plain function checkout(p, amt) never checks type, only behaviour.
Contrast: the ABC's guarantee is different in kind. An ABC + a p: PaymentProcessor type hint promises, at construction time of every subclass, that pay exists — a nominal (name/inheritance-based) guarantee checked early and loudly. Duck typing checks structurally and lazily (only when pay is actually called). ABCs move the failure earlier; duck typing tolerates any object that fits. Same goal (safe p.pay(amt)), different enforcement moment. See Interfaces in other languages for how other languages make this nominal check mandatory.
Exercise 5.4 — Register a virtual subclass (edge mechanism)
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self): ...
class Sprite: # NOT inheriting Drawable
def draw(self): return "drawn"
Drawable.register(Sprite)
print(issubclass(Sprite, Drawable)) # ?
print(isinstance(Sprite(), Drawable)) # ?
Sprite().draw() # ?Predict all three outputs, and name the one thing register does not do.
Recall Solution 5.4
issubclass(Sprite, Drawable)→True.registermakesSpritea virtual subclass for the purposes ofissubclass/isinstance.isinstance(Sprite(), Drawable)→Truefor the same reason.Sprite().draw()→"drawn"— it's an ordinary method on an ordinary class.
What register does NOT do: it does not check that Sprite actually implements draw. You could register a class with no draw at all and isinstance would still say True, then crash when you call draw(). register is a promise you make, not a check Python performs — the opposite trade-off from normal ABC enforcement. Details in Metaclasses and ABCMeta.
Quick self-test
Do these ::: reveals from memory before leaving.
Which two ingredients are both required for a class to be abstract?
ABC (or metaclass=ABCMeta) and have at least one live @abstractmethod.In a chain A -> B -> C, when is C instantiable?
C.__abstractmethods__ is empty (∅), i.e. every abstract method from the whole chain has been overridden by a non-abstract method somewhere at or above C.Does an ABC check that an override has the correct parameters?
Does Drawable.register(Sprite) verify that Sprite has the required methods?
register performs no check; it just makes issubclass/isinstance return True.Can an @abstractmethod contain reusable code?
super().Where must @abstractmethod sit relative to @property/@classmethod/@staticmethod?
def — so it tags the raw function before the other decorator wraps it.Flashcards
In Ex 3.1, what is B.__abstractmethods__?
frozenset({'g'}) — f was overridden by B, but g remains unimplemented, so B is still abstract.What does Rectangle(4, 5).area() return for the L2 solution?
20.What does Circle(3).area() return with ?
28.27431.