Home/Python/Inheritance and composition

Inheritance and composition

Substitute compatible objects and assemble behavior

Reusing behavior

Inheritance lets a subclass reuse and override base-class behavior. Composition gives an object collaborators and delegates work to them.

Use inheritance when the subclass preserves the base contract: accepted inputs, results, exceptions, and state expectations. Use composition for replaceable policies such as formatting or storage.

Python
class UpperFormatter:
    def format(self, text):
        return text.upper()

class Display:
    def __init__(self, formatter):
        self.formatter = formatter

    def show(self, text):
        return "[" + self.formatter.format(text) + "]"

print(Display(UpperFormatter()).show("hello"))  # [HELLO]

Display accepts a formatter through behavior rather than ancestry. Another compatible formatter can replace it without changing the caller. A function may suffice for a single stateless operation. If a collaborator owns resources, define who releases them.

Method resolution and super()

super() follows the instance's method resolution order (MRO), which can include sibling classes in a multiple-inheritance hierarchy:

Python
class Base:
    def trace(self):
        return ["base"]

class Left(Base):
    def trace(self):
        return ["left"] + super().trace()

class Right(Base):
    def trace(self):
        return ["right"] + super().trace()

class Combined(Left, Right):
    pass

print(Combined().trace())  # ['left', 'right', 'base']

For this instance, super() inside Left.trace reaches Right.trace. Calling Base.trace directly would skip that step. Cooperative implementations need compatible signatures, argument forwarding, and an agreed terminal method.

Gotchas

  • Code reuse alone does not establish a subtype. Stronger preconditions or changed return meanings can break existing callers.
  • Matching method names do not guarantee matching semantics. State mutation and resource ownership belong in the interface contract.
  • Exact-class branches in callers make new implementations harder to add; test through the common interface instead.
  • Shared class-level mutable state can couple subclasses. Keep hierarchies small enough that their assumptions remain visible.

Put it into practice

Implement the exercise in a full-screen workspace, then run its tests.

Open practice
Next lessonDataclasses and type hints