Zum Hauptinhalt springen

Chapter 13 — Inheritance, Composition, and Method Resolution Order

1. Opening problem

A TypeScript developer may create inheritance whenever two classes share code:

class EmailService:
def send(self, message: str) -> None:
...


class MarketingService(EmailService):
def launch_campaign(self) -> None:
...

But is a marketing service truly a kind of email service?

Probably not.

The marketing service uses an email service. It is not substitutable for one.

A better model is composition:

class MarketingService:
def __init__(
self,
email_service: EmailService,
) -> None:
self._email_service = email_service

def launch_campaign(self) -> None:
self._email_service.send(
"Campaign started"
)

Inheritance should model a meaningful relationship, not merely provide convenient code reuse.

This chapter explains:

  • substitutability;
  • inheritance contracts;
  • multiple inheritance;
  • method resolution order;
  • cooperative super();
  • mixins;
  • composition;
  • fragile base classes;
  • architectural decision-making.

2. The core question: “is-a” or “uses-a”?

Inheritance suggests:

Every instance of the subclass can be used where an instance of the base class is expected.

Example:

class NotificationSender:
def send(
self,
recipient: str,
message: str,
) -> None:
raise NotImplementedError
class EmailSender(
NotificationSender
):
def send(
self,
recipient: str,
message: str,
) -> None:
...

An EmailSender is a notification sender.

By contrast:

class UserRegistrationService(
EmailSender
):
...

is conceptually wrong. A registration service uses a sender.

Prefer composition:

class UserRegistrationService:
def __init__(
self,
sender: NotificationSender,
) -> None:
self._sender = sender

3. Substitutability

A subclass should preserve the expectations of the base class.

Suppose:

class FileStore:
def save(
self,
path: str,
content: bytes,
) -> None:
...

A subclass that silently rejects files larger than one megabyte violates callers' expectations unless the base contract allows such a restriction.

class TinyFileStore(FileStore):
def save(
self,
path: str,
content: bytes,
) -> None:
if len(content) > 1_000_000:
raise ValueError(
"File too large"
)

...

The subclass strengthens preconditions. Code that works with FileStore may fail unexpectedly with TinyFileStore.

Substitutability requires careful thought about:

  • accepted inputs;
  • returned outputs;
  • exceptions;
  • side effects;
  • performance assumptions;
  • ordering;
  • mutability;
  • transaction semantics.

4. Inheritance syntax

class Animal:
def speak(self) -> str:
return "unknown"


class Dog(Animal):
def speak(self) -> str:
return "woof"

Method overriding:

animal: Animal = Dog()
print(animal.speak())

Python resolves the method dynamically at runtime.

Unlike TypeScript, Python does not require an override keyword in ordinary syntax. Static tools may still help detect mistakes.

A misspelled method silently creates a different method:

class Dog(Animal):
def speek(self) -> str:
return "woof"

Tests and static analysis matter.


5. Calling base behavior

class BaseService:
def start(self) -> None:
print("Base setup")


class UserService(BaseService):
def start(self) -> None:
super().start()
print("User setup")

Use super() rather than naming the parent directly:

BaseService.start(self)

Why?

Because super() follows the method resolution order. Direct parent calls can break cooperative multiple inheritance.


6. Method Resolution Order

Python determines attribute lookup order through the MRO.

class A:
pass


class B(A):
pass


class C(A):
pass


class D(B, C):
pass

Inspect:

print(D.mro())

Conceptually:

D → B → C → A → object

Python uses C3 linearization. It preserves:

  • local parent ordering;
  • monotonicity;
  • consistent class hierarchy ordering.

The exact algorithm is less important than being able to inspect and reason about the resulting order.


7. The diamond problem

A
/ \
B C
\ /
D

Suppose each class implements process.

class A:
def process(self) -> None:
print("A")


class B(A):
def process(self) -> None:
print("B")
super().process()


class C(A):
def process(self) -> None:
print("C")
super().process()


class D(B, C):
def process(self) -> None:
print("D")
super().process()

Calling:

D().process()

prints:

D
B
C
A

Each implementation calls the next method in the MRO, not necessarily its lexical parent.

This is cooperative multiple inheritance.


8. super() does not mean “my parent”

In:

class B(A):
...

developers often think:

super()

means A.

That is incomplete.

super() means:

Continue lookup after the current class in the MRO for this instance.

In D(B, C), super() inside B may call C, not A.

This is the central mental model for multiple inheritance.


9. Cooperative constructors

Multiple inheritance is especially delicate with __init__.

class Named:
def __init__(
self,
*,
name: str,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.name = name
class Timestamped:
def __init__(
self,
*,
created_at,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.created_at = created_at
class Entity(
Named,
Timestamped,
):
def __init__(
self,
*,
entity_id: int,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.entity_id = entity_id

Construction:

entity = Entity(
entity_id=1,
name="Example",
created_at="2026-07-29",
)

Cooperative rules:

  • every class calls super();
  • each class consumes only its parameters;
  • remaining keyword arguments are forwarded;
  • signatures are compatible;
  • the final base can accept no remaining arguments.

This is powerful but can be difficult to maintain.


10. Mixins

A mixin is a focused class providing reusable behavior.

class JsonSerializableMixin:
def to_json(self) -> str:
import json
return json.dumps(
self.to_dict()
)

Expected collaborator:

class User(
JsonSerializableMixin
):
def to_dict(
self,
) -> dict[str, object]:
return {
"id": self.user_id,
"email": self.email,
}

A good mixin is:

  • small;
  • focused;
  • behavior-oriented;
  • not independently instantiated;
  • explicit about required methods;
  • free from large hidden state.

A mixin should not become a secret base service with many dependencies.


11. Typing mixin requirements

A mixin may rely on a protocol.

from typing import Protocol


class SupportsToDict(Protocol):
def to_dict(
self,
) -> dict[str, object]:
...

A helper function may be clearer than a mixin:

import json


def to_json(
value: SupportsToDict,
) -> str:
return json.dumps(
value.to_dict()
)

Choose a mixin when method syntax meaningfully improves the API.

Choose a function when composition is clearer.


12. Abstract base classes

from abc import ABC, abstractmethod


class Repository(ABC):
@abstractmethod
def save(
self,
entity: object,
) -> None:
...

Subclass:

class PostgresRepository(
Repository
):
def save(
self,
entity: object,
) -> None:
...

An abstract class can provide:

  • runtime instantiation restrictions;
  • shared implementation;
  • nominal identity;
  • framework integration;
  • class-level contracts.

However, a Protocol often provides looser structural typing without inheritance.

Use an ABC when runtime nominal relationships matter.

Use a protocol when behavior compatibility matters.


13. Composition

class ReportService:
def __init__(
self,
renderer,
storage,
notifier,
) -> None:
self._renderer = renderer
self._storage = storage
self._notifier = notifier

Composition makes dependencies visible.

Benefits:

  • isolated tests;
  • replaceable components;
  • shallow class hierarchies;
  • independent lifecycle;
  • explicit ownership;
  • easier reasoning.

Trade-off:

  • more forwarding code;
  • more objects;
  • dependency wiring.

The extra explicitness is often worthwhile.


14. Delegation

Composition sometimes needs delegation:

class CachedRepository:
def __init__(
self,
inner_repository,
cache,
) -> None:
self._inner = inner_repository
self._cache = cache

def find(
self,
entity_id: int,
):
cached = self._cache.get(
entity_id
)

if cached is not None:
return cached

entity = self._inner.find(
entity_id
)

if entity is not None:
self._cache.set(
entity_id,
entity,
)

return entity

The wrapper has the same behavioral contract but composes an inner implementation.

This is often safer than inheriting from a concrete database repository.


15. Fragile base-class problem

A base-class change can break subclasses unexpectedly.

class Base:
def save(self) -> None:
self.validate()
self.persist()

A subclass overrides validate with assumptions that no longer hold after a base-class refactor.

The problem becomes worse when base constructors call overridable methods:

class Base:
def __init__(self) -> None:
self.configure()

def configure(self) -> None:
...

Subclass state may not yet be initialized when configure is called.

Avoid calling overridable methods from constructors unless the lifecycle is carefully designed.


16. Template method pattern

A controlled inheritance use:

from abc import ABC, abstractmethod


class ImportJob(ABC):
def run(self) -> None:
raw = self.load()
parsed = self.parse(raw)
self.validate(parsed)
self.save(parsed)

@abstractmethod
def load(self):
...

@abstractmethod
def parse(self, raw):
...

def validate(self, parsed) -> None:
pass

@abstractmethod
def save(self, parsed) -> None:
...

This can be useful when:

  • the algorithm skeleton is stable;
  • extension points are explicit;
  • lifecycle is documented;
  • subclasses remain small.

Composition with strategy objects may still be more flexible.


17. Multiple inheritance risk checklist

Before using multiple inheritance, ask:

  1. Are parent classes designed for cooperation?
  2. Do all relevant methods use super()?
  3. Are constructor signatures compatible?
  4. Is the MRO obvious?
  5. Are mixins focused and mostly stateless?
  6. Can composition solve the problem more clearly?
  7. Will future contributors understand the hierarchy?
  8. Are tests covering the full MRO behavior?

If several answers are “no,” avoid the design.


18. TypeScript comparison

TypeScript supports single class inheritance and multiple interface implementation.

Python supports multiple class inheritance.

TypeScript commonly uses:

class Service extends BaseService
implements Auditable, Cacheable {}

Python may use:

  • one concrete base class;
  • several mixins;
  • protocols for static contracts;
  • composition for dependencies.

Because Python permits more inheritance flexibility, discipline matters more.


19. Common mistakes

Inheriting for code reuse

Prefer a helper, composition, or delegation.

Direct parent calls

Use cooperative super() where multiple inheritance is possible.

Stateful mixins

Keep mixins focused.

ABCs everywhere

Use protocols where nominal runtime identity is unnecessary.

Constructor method calls

Avoid invoking overridable methods before subclass initialization.

Deep hierarchies

Prefer flatter designs.

Ignoring MRO

Inspect Class.mro().


20. English vocabulary

TermMeaning
inheritancederiving behavior and structure from a base class
substitutabilityability to use a subtype where a base type is expected
method resolution orderclass lookup order
cooperative inheritanceclasses forwarding through super()
mixinfocused class adding reusable behavior
delegationforwarding behavior to a collaborator
compositionbuilding an object from other objects
fragile base classbase change unexpectedly breaking subclasses
hierarchyinheritance tree
extension pointintended place for customization

Useful sentences:

  • “The subclass strengthens the precondition and breaks substitutability.”
  • super() continues lookup through the MRO.”
  • “This mixin is too stateful and should become a collaborator.”
  • “Composition makes ownership and lifecycle explicit.”
  • “The base constructor calls an overridable method too early.”
  • “The hierarchy exists only for code reuse.”

21. Speaking task

Explain for nine minutes:

Why super() does not simply mean “call my parent.”

Include the diamond hierarchy and cooperative constructors.


22. Writing task

Write a 400-word architecture review of a service hierarchy with six inheritance levels, shared database state, and mixins that define constructors.


23. Exercises

Exercise 1

Predict the output of a diamond hierarchy using cooperative super().

Exercise 2

Refactor a notification inheritance hierarchy into composition.

Exercise 3

Create a focused AuditMixin that records an audit entry after a method succeeds.

Exercise 4

Design cooperative Named, Timestamped, and Entity constructors.

Exercise 5

Choose between an ABC and a protocol for a payment processor and explain your choice.


24. Complete solutions

Solution 1

class A:
def run(self) -> None:
print("A")


class B(A):
def run(self) -> None:
print("B")
super().run()


class C(A):
def run(self) -> None:
print("C")
super().run()


class D(B, C):
def run(self) -> None:
print("D")
super().run()


D().run()

Output:

D
B
C
A

Solution 2

from typing import Protocol


class Sender(Protocol):
def send(
self,
recipient: str,
message: str,
) -> None:
...


class NotificationService:
def __init__(
self,
sender: Sender,
) -> None:
self._sender = sender

def notify(
self,
recipient: str,
message: str,
) -> None:
self._sender.send(
recipient,
message,
)

The service uses a sender instead of pretending to be one.

Solution 3

A mixin should avoid hidden constructor requirements:

class AuditMixin:
def write_audit(
self,
message: str,
) -> None:
audit_writer = getattr(
self,
"_audit_writer",
)

audit_writer.write(message)

A normal collaborator is often clearer:

class AuditedService:
def __init__(
self,
audit_writer,
) -> None:
self._audit_writer = audit_writer

The exercise demonstrates that composition may be preferable.

Solution 4

class Named:
def __init__(
self,
*,
name: str,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.name = name


class Timestamped:
def __init__(
self,
*,
created_at: str,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.created_at = created_at


class Entity(
Named,
Timestamped,
):
def __init__(
self,
*,
entity_id: int,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.entity_id = entity_id

Solution 5

A protocol is usually appropriate when any object with a compatible charge method should work:

from typing import Protocol


class PaymentProcessor(
Protocol
):
def charge(
self,
amount_cents: int,
) -> str:
...

Use an ABC if runtime nominal identity, shared implementation, or registration behavior is required.


25. Chapter checkpoint

You should now be able to explain:

  1. substitutability;
  2. inheritance versus composition;
  3. MRO;
  4. cooperative super();
  5. the diamond problem;
  6. cooperative constructors;
  7. mixins;
  8. ABCs versus protocols;
  9. fragile base classes;
  10. inheritance decision criteria.