Abstraction
Three products, three monthly_fee() methods, one billing loop that never asks
which is which. It holds together because every product happens to have the
method.
Happens to. Nothing wrote that rule down, and nothing checks it. Ship a fourth product with the method misspelled and the billing loop finds out at month end, in production, holding an object it cannot bill.
Abstraction is two habits at once:
- name what a type must be able to do, and make that a rule the language enforces;
- hide how it does it, so callers cannot come to depend on the how.
A base class with holes in it
The abc module — abstract base classes — lets you declare a method that every
subclass is required to fill in:
Product is a contract, not a product. Inheriting from ABC and marking
monthly_fee with @abstractmethod states the rule: whatever you are, you
charge a fee, and you are the one who says what it is.
The contract is checked, and it is checked early
Leave the hole unfilled and Python refuses to build the object at all:
from abc import ABC, abstractmethod
class Product(ABC):
@abstractmethod
def monthly_fee(self):
...
class Draft(Product):
pass
print(Draft().monthly_fee())That is the trade the previous lesson left open. Duck typing asks for the method at the moment of the call, which can be much later and somewhere else. An abstract base class asks at construction, next to the class that got it wrong.
An abstract class is not only holes
It can carry real, shared code — and that code may call the holes:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
...
def describe(self):
return f"area {self.area()}"
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side * self.side
print(Square(3).describe())Name the method wrong and you have not implemented the contract — you have added an unrelated method and left the hole open:
TypeErrorRead the error — it names the method that's missing. Then make it print `fee 0.0`.
Hiding the how
That is the second half, and the half that survives contact with real systems. Two stores, same contract, completely different insides — and the caller cannot tell them apart:
get_or is written once, against the contract rather than against a dict. Swap
either store for one backed by a database and it does not change — because it
was never allowed to know there was a dict in there.
When not to reach for it
Python does not need any of this to make the billing loop work; that was the
previous lesson, and duck typing is the more common Python answer. Reach for
ABC when you want the mistake caught at construction, and the contract written
down where the next person will look for it.
When you want the contract without the inheritance, typing.Protocol describes
the same thing structurally — anything with the right methods satisfies it, no
base class involved.
🎯 Your turn
Define an abstract Product with an abstract label() and a concrete
summary() returning f"{self.label()} account". Give Current and Savings
their labels; leave Draft without one. Write try_open(kind) returning that
product's summary, or "incomplete" if the class cannot be built:
try_open("current")→"current account"try_open("savings")→"savings account"try_open("draft")→"incomplete"
Hint — the failure is a TypeError, and it happens on Draft(), before any
method is called. Wrap the construction, not the summary().
Then press ▶ Run and hit ✓ Check. ✅
