Both inheritance and composition reuse code, but they couple classes differently. Inheritance (is-a) binds a subclass to the internals of its base. Composition (has-a) keeps an object inside another and uses only its public interface. The basics are in Composition vs inheritance; this lesson is about the design decision.

Left: Penguin inherits Bird.fly and must raise an error. Right: Bird has a Mover, Fly and Walk implement it
Left: Penguin inherits Bird.fly and must raise an error. Right: Bird has a Mover, Fly and Walk implement it

When inheritance is justified

  • Real is-a. Every subclass fulfils every promise of the base class, so it can be used wherever the base is expected: the Liskov Substitution Principle.
  • A stable base designed for extension: Exception, abstract base classes, framework hooks such as unittest.TestCase.
  • A shallow tree: one or two levels.
python
class Bird:
    def fly(self):
        return "flap"

class Sparrow(Bird):
    pass

class Penguin(Bird):
    def fly(self):
        raise NotImplementedError("penguins cannot fly")

for b in [Sparrow(), Penguin()]:
    try:
        print(type(b).__name__, b.fly())
    except NotImplementedError as e:
        print(type(b).__name__, "fails:", e)
text
Sparrow flap
Penguin fails: penguins cannot fly

Penguin inherits a fly() it cannot honour. Every caller that loops over Bird objects now needs a special case. When a subclass raises NotImplementedError or silently does nothing in an inherited method, the "is-a" was only an illusion.

The fragile base class

A subclass depends on how the base is implemented, not only on what it promises. Here Bag.add_all calls self.add, and the subclass overrides both methods, so every item is counted twice.