Inheritance or composition?
Inherit when the new class truly is a kind of the parent and should work everywhere the parent does. When you only want some of another class's behaviour, hold an object of it and expose just what you need.
- Apply the is-a / has-a test to decide between a subclass and a part
- Recognise a subclass that inherits methods which break its own rules
- Rewrite such a class with composition, exposing only the operations it should allow
Two questions decide it:
- Is it a kind of the parent? A book is a product; a card payment is a payment. Then every method the parent has should make sense for the child, and code written for the parent should work unchanged with the child. Inherit.
- Does it only use the other class's behaviour? An audit log stores entries in a list; a shipment uses a carrier. It *has* one. Hold an object and delegate.
Inheriting just to reuse code is a common mistake. The child gets every method of the parent, including ones that break its rules. An audit log must only ever grow, but a subclass of list also inherits insert, remove and clear.
Run each example to see what it prints. Running examples doesn't count toward progress.
class AuditLog(list): def record(self, user, action): self.append(f"{user}: {action}") log = AuditLog() log.record("nora", "refund O100376") log.record("jamal", "edit price SPO-11681") log.insert(0, "admin: nothing happened") log.remove("nora: refund O100376") print(len(log), log[0])
With composition the log has a list and offers only the operations an audit log should have. The list is still doing the storage, but nobody outside can call insert on it through the log.
class AuditLog: def __init__(self): self.entries = [] def record(self, user, action): self.entries.append(f"{user}: {action}") def latest(self, n): return self.entries[-n:] def count(self): return len(self.entries) log = AuditLog() log.record("nora", "refund O100376") log.record("jamal", "edit price SPO-11681") log.record("grace", "cancel O101219") print(log.count(), log.latest(2)) print(hasattr(log, "insert"), hasattr(log, "remove"))
A practical rule: prefer composition, and use inheritance when the is-a relationship is real and the child can honour every promise the parent makes. Composition also lets parts change at run time (swap a carrier, change an employee's role), which a class hierarchy can't.
Mistakes to avoid: subclassing built-in containers (list, dict) to get storage; deep hierarchies (Product → ShippedProduct → HeavyShippedProduct → …) where every new feature needs a new level; and subclasses that refuse or change the meaning of a parent method, so code written for the parent breaks on them.
- Is-a: inherit, and make sure the child keeps every promise of the parent.
- Has-a: hold the object and delegate, exposing only the operations that make sense.
- Subclassing containers to get storage exposes methods that can break your rules; composition doesn't.
- Next module: dataclasses, properties and type hints for describing data models with less code.