Course progress Course outline 24 of 24 lessons available
Python Foundations
Data and Collections
Building Reliable Programs
Modeling with Objects
Professional Python
Advanced Python
Build a robot from replaceable helpers
Imagine a study robot holding a score calculator. The robot has a calculator; it is not a kind of calculator. That relationship is composition.
- Main object a study plan
- Helper a score rule
- Has-a plug in another helper
Receive the helper in __init__() and delegate work to it:
class PlainHeading:
def make(self, text):
return text
class Report:
def __init__(self, heading_maker):
self.heading_maker = heading_maker
def title(self, text):
return self.heading_maker.make(text)
print(Report(PlainHeading()).title("Study plan"))
Study plan
The dependency is visible and replaceable. Passing a collaborator from outside is a simple form of dependency injection. Tests can inject a tiny fake; production code can inject a real service.
Swap helpers that keep one promise
Another helper may do the same job differently:
class PlainHeading:
def make(self, text):
return text
class LoudHeading:
def make(self, text):
return text.upper() + "!"
for helper in [PlainHeading(), LoudHeading()]:
print(helper.make("Study plan"))
Study plan
STUDY PLAN!
This is polymorphism: one call works with different objects because they offer a compatible capability. No shared base class is required.
-
Contract
make(text) - Accept one string
- Return one heading string
- Same meaning no surprise side effects
A helper named make() that returns None, deletes data, or prints instead of returning text breaks the contract. Test every intended implementation with the same small examples.
Inherit only for a true is-a relationship
Inheritance says that a subclass really is a specialized base object and can safely stand in for it. A reading activity can be a kind of activity:
class Activity:
def __init__(self, title, minutes):
if minutes < 0:
raise ValueError("minutes must not be negative")
self.title = title
self.minutes = minutes
def summary(self):
return f"{self.title}: {self.minutes} min"
class ReadingActivity(Activity):
def __init__(self, title, minutes, pages):
super().__init__(title, minutes)
self.pages = pages
def summary(self):
return f"{self.title}: {self.pages} pages"
item = ReadingActivity("Python docs", 25, 12)
print(item.summary())
print([cls.__name__ for cls in ReadingActivity.mro()])
Python docs: 12 pages
['ReadingActivity', 'Activity', 'object']
The subclass overrides summary(). super() continues lookup after the current class according to the method resolution order, or MRO. It is more accurate than saying “call my parent,” especially with multiple inheritance.
- ReadingActivity look here first
- Activity then the base
- object the final base
Use the substitution test: code promised an Activity; can it receive ReadingActivity without new type checks or a weaker meaning? If not, the “is-a” claim is probably false.
Prefer small capabilities to tall family trees
Inheritance is not a coupon for free code reuse. Warning signs include a subclass that overrides almost everything, repeated isinstance() branches, or names such as LoudJsonScoredReport multiplying for every option combination.
Formatting, storage, and scoring are independent helpers. Compose them so each can change alone. A capability-based function simply asks for the operation it needs:
def show(result):
print(result.render())
Any object with a compatible render() can participate. Keep that contract small and keep inheritance chains shallow. Multiple inheritance has a deterministic C3 MRO, but it also adds cooperation rules; reach for it only when the model truly needs it.
Tiny project: Composable Study Scoring
Create study_scoring.py. This complete program uses only built-in Python features. Score rules and reporters are injected helpers; the plan never branches on their exact classes.
class FixedScore:
def __init__(self, points):
if isinstance(points, bool) or not isinstance(points, int) or points < 0:
raise ValueError("points must be a non-negative integer")
self.points = points
def calculate(self, minutes):
return self.points
class MinuteScore:
def __init__(self, minutes_per_point):
if (
isinstance(minutes_per_point, bool)
or not isinstance(minutes_per_point, int)
or minutes_per_point <= 0
):
raise ValueError("minutes_per_point must be a positive integer")
self.minutes_per_point = minutes_per_point
def calculate(self, minutes):
return minutes // self.minutes_per_point
class TextReport:
def render(self, rows):
lines = [
f"{title}: {minutes} min -> {points} pts"
for title, minutes, points in rows
]
lines.append(f"Total: {sum(points for _, _, points in rows)} pts")
return "\n".join(lines)
class CompactReport:
def render(self, rows):
total = sum(points for _, _, points in rows)
return f"{len(rows)} activities / {total} pts"
class StudyPlan:
def __init__(self, score_rule, reporter):
self.score_rule = score_rule
self.reporter = reporter
def report(self, sessions):
rows = [
(title, minutes, self.score_rule.calculate(minutes))
for title, minutes in sessions
]
return self.reporter.render(rows)
def verify_rule(rule):
result = rule.calculate(20)
assert not isinstance(result, bool)
assert isinstance(result, int) and result >= 0
def verify_reporter(reporter):
assert isinstance(reporter.render([("Test", 10, 2)]), str)
sessions = [("Python", 45), ("Reading", 30)]
minute_plan = StudyPlan(MinuteScore(5), TextReport())
fixed_plan = StudyPlan(FixedScore(3), CompactReport())
print(minute_plan.report(sessions))
print(fixed_plan.report(sessions))
for rule in [MinuteScore(5), FixedScore(3)]:
verify_rule(rule)
for reporter in [TextReport(), CompactReport()]:
verify_reporter(reporter)
print("Contracts passed.")
Run python3 study_scoring.py and check:
Python: 45 min -> 9 pts
Reading: 30 min -> 6 pts
Total: 15 pts
2 activities / 6 pts
Contracts passed.
Boundary reminder: MinuteScore(0) is rejected. A new collaborator must satisfy the whole behavior contract, not merely own a correctly named method.
Three tiny missions
- Swap a helper. Add
BonusScorewithcalculate(minutes)without editingStudyPlan. - Test is-a. Create
VideoActivity(Activity)and state what everyActivitycaller may still rely on. - Read the path. Add one more subclass and print its
mro()before predicting which override runs.
You are ready for Chapter 15 when…
- you can tell has-a composition from is-a inheritance;
- you can inject and replace a collaborator;
- you can state and test a behavioral contract;
- you can use capability-based polymorphism without a shared base class;
- you reserve inheritance for substitutable subtypes;
- you can explain overriding,
super(), and a simple MRO; - you avoid deep hierarchies and exact-type switch chains;
- you can run both scoring plans and see
Contracts passed.
Next, you will make compact data records with dataclasses and replace loose choice strings with enums.