Composition and Inheritance
Choose relationships that let objects collaborate without creating fragile hierarchies.
By the end, you’ll be able to…
- Use composition to replace a collaborator.
- Apply inheritance only when a subtype preserves the parent contract.
- Explain overriding, super(), and behavioral compatibility.
01Compose a system from collaborators
Composition means one object uses another to perform part of its work. A report service can receive a formatter rather than inherit from a formatter. The service owns the reporting workflow, and the collaborator owns formatting. Supplying the collaborator through the constructor makes that dependency explicit and replaceable.
This design helps testing as well as reuse. A small in-memory collaborator can record calls without touching a network or file. Focus on the behavior the collaborator must provide, not its concrete class name. In Python, an object with the required method can often participate without inheriting a common base.
02Inheritance promises substitutability
Inheritance creates a subtype relationship and reuses behavior through attribute lookup. A subclass can override a method, but the important question is whether callers using the parent contract still receive acceptable behavior. A read-only archive that inherits a writable store but rejects every save is a poor substitute for callers promised a writable store.
A valid override should not unexpectedly demand stricter inputs, return an incompatible kind of result, or remove essential effects. Shared code alone is a weak reason for inheritance. If two concepts merely need the same helper, a module function or composed helper may express the relationship more honestly.
03super participates in method resolution
Calling super().render delegates to the next implementation in the method resolution order. In simple single inheritance that is the parent implementation. It is more accurate to think "next implementation" than to assume super always means one fixed parent, because multiple inheritance can arrange a longer cooperative chain.
When a subclass defines __init__, it is responsible for arranging any required parent initialization, often through super().__init__(...). Otherwise parent attributes may never be set. Keep beginner hierarchies shallow; a deep chain makes it harder to know which override executes and which state every constructor must establish.
04Compare the cost of change
Choose composition when behavior varies independently: a storage backend, discount policy, renderer, or clock can be supplied as a collaborator. Choose inheritance when the domain has a stable subtype relationship and a useful common contract. Both techniques can coexist: a specialized report may inherit a report interface while composing a storage service.
Before adding a subclass, write a small caller that uses only the proposed parent interface. Can it handle both objects without special-case isinstance branches? If every caller must detect the subclass and compensate for surprises, the abstraction is not doing its job. Test shared contract behaviors against each implementation, including failure behavior, rather than testing only that inheritance syntax works.
The ideas worth keeping.
- Compose replaceable behaviors through small collaborator interfaces.
- Use inheritance when subclasses preserve a meaningful contract.
- Keep initialization and method-resolution behavior explicit.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Replace a delivery dependency
Build Reminder(sender) with remind(name). Inject a RecordingSender that collects messages; verify the reminder without performing real delivery.
Explore a worked solution
class RecordingSender: def __init__(self): self.messages = [] def send(self, message): self.messages.append(message)class Reminder: def __init__(self, sender): self.sender = sender def remind(self, name): self.sender.send(f"{name}, review your notes")sender = RecordingSender()Reminder(sender).remind("Ada")assert sender.messages == ["Ada, review your notes"]The workflow depends only on send(message). Composition makes the effect observable while keeping delivery outside the reminder logic.
Extend parent initialization safely
Create Member(name) and PremiumMember(name, credits). Preserve the parent label() contract and initialize both pieces of state.
Explore a worked solution
class Member: def __init__(self, name): self.name = name def label(self): return self.nameclass PremiumMember(Member): def __init__(self, name, credits): super().__init__(name) self.credits = credits def label(self): return super().label() + " (premium)"member = PremiumMember("Lin", 5)assert member.label() == "Lin (premium)"assert member.credits == 5The subclass establishes the parent state before adding its own. label still accepts the same arguments and returns a usable string.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Replace a delivery dependency
Build Reminder(sender) with remind(name). Inject a RecordingSender that collects messages; verify the reminder without performing real delivery.
[Process exited with code 0 - No output]
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
One chapter closer.
Reading is a start. Trying it yourself makes the connection.