Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Objects  ›  Lesson

Abstraction

Abstraction 16 minNaming what a type must do, and hiding how it does it
You're building a piece ofA bank account that can't go wrong
This piece — Product(ABC): A product that forgets its fee is refused at construction, not at 3am.
Scenario A fourth product ships with monthly_fee() misspelled. Nothing complains until a customer's statement crashes at 3am — by which time the broken object is already sitting in a list somewhere.
Your task
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. Example: try_open("draft") → "incomplete".

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:

python

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:

What happens when this runs?
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:

What does this print?
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:

broken — fix itTypeError

Read 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:

step through it
1from abc import ABC, abstractmethod
2 
3class Store(ABC):
4 @abstractmethod
5 def get(self, key):
6 ...
7 
8 def get_or(self, key, fallback):
9 found = self.get(key)
10 return fallback if found is None else found
11 
12class DictStore(Store):
13 def __init__(self):
14 self.rows = {"a": 1}
15 
16 def get(self, key):
17 return self.rows.get(key)
18 
19class PairStore(Store):
20 def __init__(self):
21 self.rows = [("a", 1)]
22 
23 def get(self, key):
24 for k, v in self.rows:
25 if k == key:
26 return v
27 return None
28 
29for store in (DictStore(), PairStore()):
30 print(store.get_or("a", 0), store.get_or("z", 0))

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

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upDeclare the hole, then fill it

Define Shape(ABC) with an abstract area(). Define Square(Shape) built from a side, implementing area(). Write square_area(side) returning that area.

abstractionabc
DrillThe contract bites

Define Report(ABC) with an abstract render(). Define Html(Report) implementing it, and Draft(Report) which does not. Write can_build(kind) returning True if the named class can be instantiated and False if it raises.

abstractionabcexceptions
BuildThe base owns the algorithm

Define Exporter(ABC) with an abstract body() and a concrete document() returning f"<{self.body()}>". Define Csv(Exporter) whose body is "a,b" and Json(Exporter) whose body is '{"a": 1}'. Write documents() returning both documents as a list, Csv first.

abstractionabcpolymorphism
BossTwo insides, one contract

Define Store(ABC) with an abstract get(key) and a concrete get_or(key, fallback) returning the fallback when get gives None. Define DictStore holding {"a": 1, "b": 2} in a dict and PairStore holding the same data as a list of pairs. Write lookup(backend, key)"dict" or "pairs" — returning the value, or "missing".

abstractionabcpolymorphism
CapstoneNo product bills by accident

Define Account(ABC) built from a balance, with an abstract monthly_fee() and a concrete after_month() returning the balance minus the fee, rounded to 2 decimals. Define Current (fee 5.0), Savings (fee 0.0) and Draft, which never implements the fee. Write run_month(entries) where entries look like [["current", 100]], returning each balance after the month — or "incomplete" for a product that cannot be built.

abstractionabcpolymorphismexceptions
try_open.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
try_open(kind) → strOpen the named product, or report that it never honoured the contract.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.