Today's outcomes
- Explain an is-a relationship and an overridden method.
- Use super to cooperate with the parent implementation.
- Recognize when has-a composition is clearer.
Before you start: Complete Day 20 and its practical challenge.
Understand the concept
Inheritance expresses a specialization relationship
A subclass can inherit methods and attributes from a base class and provide specialized behavior. Use inheritance when the subclass genuinely fits the base class's role in the application. A timed task can still be a task; a report containing several tasks is not itself one of those tasks.
The role matters more than the label “reuse.” A subclass that unexpectedly rejects operations accepted by its base can break callers that depend on the base interface. Document the accepted inputs and observable behavior, then test specialized objects where ordinary base objects are expected.
Super cooperates with the method resolution order
super() lets a method delegate to the next applicable implementation in the object's method resolution order, or MRO. In a simple single-inheritance chain this often looks like calling the parent. In multiple inheritance, the next implementation may be another cooperating class rather than the parent you imagined from one definition.
Use super in initialization when the base establishes required state. Do not manually duplicate its validation merely because the subclass has additional fields. A later base change should not require finding several copied versions of the same rule.
Composition expresses ownership or collaboration
Composition stores a collaborating object as a field. A StudyPlan may have a notifier, rather than being a notifier. A report may contain tasks, rather than inheriting from a task. Composition lets you reuse behavior while keeping responsibilities distinct.
There is no rule that inheritance is advanced and composition is basic, or the reverse. Choose the relationship that makes the public contract easiest to understand and change. A small program can often use ordinary functions and dictionaries without either pattern.
| Relationship | Typical structure | Question to ask |
|---|---|---|
| Single inheritance | One direct base | Is the specialized object still valid in the base role? |
| Multilevel inheritance | Subclass of a subclass | Does each added layer preserve the contract? |
| Hierarchical inheritance | Several subclasses of one base | Is shared behavior genuinely common? |
| Multiple inheritance | Several direct bases | Do methods cooperate through a consistent MRO? |
| Composition | One object stores another | Is this a has-a or uses-a relationship? |
Worked example
class Task:
def __init__(self, title):
if not isinstance(title, str) or not title.strip():
raise ValueError("title must be nonempty text")
self.title = title.strip()
def describe(self):
return self.title
class TimedTask(Task):
def __init__(self, title, minutes):
super().__init__(title)
if type(minutes) is not int or minutes < 0:
raise ValueError("minutes must be a nonnegative integer")
self.minutes = minutes
def describe(self):
return f"{super().describe()} ({self.minutes} min)"
task = TimedTask(" Read functions ", 30)
assert isinstance(task, Task)
assert task.title == "Read functions"
assert task.describe() == "Read functions (30 min)"
print(task.describe())
The base handles the title contract, and the subclass adds a duration contract. The overridden describe method reuses the base description before extending it. This is a small, understandable chain with a concrete relationship.
Here is an independent MRO demonstration without state or constructors:
class Root:
def labels(self):
return ["root"]
class Named(Root):
def labels(self):
return ["named"] + super().labels()
class Logged(Root):
def labels(self):
return ["logged"] + super().labels()
class Combined(Named, Logged):
pass
assert Combined().labels() == ["named", "logged", "root"]
print([kind.__name__ for kind in Combined.__mro__])
In Combined's MRO, super from Named continues through Logged before Root. This shows why “super always calls my direct parent” is an incomplete explanation. Multiple inheritance requires cooperation and should not be introduced merely to avoid a few repeated lines.
Guided practice
- Predict: trace the title initialization and description call in TimedTask. Identify which implementation owns each validation.
- Modify: create an UntimedTask subclass without overriding describe. Verify that inherited behavior remains available and returns the title.
- Reconstruct: create a TaskReport object that stores a list of Task instances and renders their descriptions. Explain why containing tasks is composition rather than a reason to inherit from Task.
When deciding between designs, write a caller example. If the caller only needs a describe method, several objects might share that behavior without needing one common base. Tomorrow's polymorphism lesson examines that possibility.
Daily challenge
Create PriorityTask(Task) with title validation inherited from Task and a priority of "low", "normal", or "high". Default to normal. Override describe to return [priority] title. Invalid priority must raise ValueError. Preserve the base's title trimming behavior.
| Creation | Expected result |
|---|---|
| title Read, default priority | [normal] Read |
title Test , high priority |
[high] Test |
| blank title | ValueError from the title contract |
| priority urgent | ValueError from the priority contract |
Do not copy the base's title validation into the subclass. Use its initializer through super. Include one test that checks the specialized object is also a Task.
Reference solution
Compare after attempting the challenge
class Task:
def __init__(self, title):
if not isinstance(title, str) or not title.strip():
raise ValueError("title must be nonempty text")
self.title = title.strip()
def describe(self):
return self.title
class PriorityTask(Task):
def __init__(self, title, priority="normal"):
super().__init__(title)
if priority not in {"low", "normal", "high"}:
raise ValueError("unsupported priority")
self.priority = priority
def describe(self):
return f"[{self.priority}] {super().describe()}"
assert PriorityTask("Read").describe() == "[normal] Read"
special = PriorityTask(" Test ", "high")
assert special.describe() == "[high] Test" and isinstance(special, Task)
for title, priority in [(" ", "normal"), ("Read", "urgent")]:
try:
PriorityTask(title, priority)
except ValueError:
pass
else:
raise AssertionError("Invalid task should fail")
print("Specialized task contract passed")
Debug and explain
If a subclass lacks the base's fields, inspect whether its initializer delegates correctly. If an overridden method loops recursively, inspect whether you called self.describe from inside describe rather than delegating appropriately. If a multiple-inheritance result surprises you, inspect __mro__.
Explain the difference between an is-a and has-a relationship with your own example. Identify one obligation a subclass owes callers of the base interface. Then describe why shared behavior does not always require shared ancestry.
Review and next step
Save the specialization tests and MRO trace. Rebuild the single-inheritance example before exploring more complex chains. Day 22 focuses on interfaces and polymorphism, including the limits of same-name method definitions in Python.
Sources and verification
Original teaching examples checked with Python 3.12.14. Library behavior reviewed against these primary references on October 3, 2026. Day 26's examples also use Beautiful Soup 4.14.3.
Check your understanding
Five questions, 20 points each. Pass at 85% or above, which requires all five correct. Feedback explains each answer; revise the lesson and retry when needed.
Review your practical work
Check these statements after running your own challenge. This is your self-review; the website does not execute or assess your Python code.
Pass the knowledge check and finish the practical self-review to save completion.