2.1.13 · D4OOP Fundamentals

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

3,151 words14 min readBack to topic

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 class C?" Read can_instantiate(C) as "is C buildable?" — C is the input, and the output is True or False.
  • (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:

Figure — Abstract base classes — ABC module, `@abstractmethod`
Figure s01 — A class 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. A decorates f with @abstractmethod but ==does not inherit ABC==. The decorator only tags the function; the blocking is done by the metaclass ABCMeta, which arrives with ABC. Since A's metaclass is the ordinary type, nobody ever reads the tag (and no __abstractmethods__ checklist is even stapled on). A() succeeds.
  • (b) ABSTRACT. B inherits ABC and has an @abstractmethod. So B.__abstractmethods__ = frozenset({'f'}), which is non-empty → B() raises TypeError.
  • (c) NOT abstract. C inherits ABC but has zero abstract methods (f is 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()   # 20

Why 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.27431

Why: 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 0

Can 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'})B overrides f (removes f); g is still inherited-but-unimplemented, so it survives. B is still abstract.
  • C.__abstractmethods__ = frozenset() () — C overrides g, and f was already cleared by B. 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.

Figure — Abstract base classes — ABC module, `@abstractmethod`
Figure s02 — Step-by-step tracing of the one rule down the chain 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. register makes Sprite a virtual subclass for the purposes of issubclass/isinstance.
  • isinstance(Sprite(), Drawable)True for 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?
Inherit ABC (or metaclass=ABCMeta) and have at least one live @abstractmethod.
In a chain A -> B -> C, when is C instantiable?
When 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?
No — only that the name is overridden. Signatures are your (or mypy's) responsibility.
Does Drawable.register(Sprite) verify that Sprite has the required methods?
No — register performs no check; it just makes issubclass/isinstance return True.
Can an @abstractmethod contain reusable code?
Yes — subclasses still must override it, but they can call the shared body via super().
Where must @abstractmethod sit relative to @property/@classmethod/@staticmethod?
Innermost — touching 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.