|$ curl https://forge-ai.dev/api/markdown?path=docs/python/inheritance
$cat docs/python-—-inheritance-&-polymorphism.md
updated Recently·20 min read·published

Python — Inheritance & Polymorphism

PythonAdvanced
Introduction

Inheritance is a core pillar of object-oriented programming that allows a class to derive behaviour from another class. Python supports single, multiple, and multilevel inheritance with a sophisticated method resolution order (MRO) powered by the C3 linearization algorithm.

This section covers everything from basic single inheritance through advanced topics such as the diamond problem, mixins, abstract base classes, and the composition-over-inheritance principle.

Single Inheritance

A child class inherits attributes and methods from a single parent class. The child can override or extend the parent's behaviour.

single.py
Python
1class Animal:
2 def __init__(self, name: str):
3 self.name = name
4
5 def speak(self) -> str:
6 return f"{self.name} makes a sound."
7
8class Dog(Animal):
9 def speak(self) -> str:
10 return f"{self.name} barks."
11
12class Cat(Animal):
13 def speak(self) -> str:
14 return f"{self.name} meows."
15
16rex = Dog("Rex")
17whiskers = Cat("Whiskers")
18
19print(rex.speak()) # → Rex barks.
20print(whiskers.speak()) # → Whiskers meows.
21
22# isinstance check
23print(isinstance(rex, Dog)) # True
24print(isinstance(rex, Animal)) # True

info

All classes in Python implicitly inherit from object. The isinstance() function respects the full inheritance chain.
super() & Cooperative Inheritance

The super() built-in returns a proxy object that delegates method calls to the next class in the MRO. It is essential for cooperative multiple inheritance and for calling the parent implementation from an overridden method.

super.py
Python
1class Animal:
2 def __init__(self, name: str):
3 self.name = name
4
5class Dog(Animal):
6 def __init__(self, name: str, breed: str):
7 super().__init__(name) # delegate to Animal.__init__
8 self.breed = breed
9
10 def __repr__(self) -> str:
11 return f"Dog(name={self.name}, breed={self.breed})"
12
13rex = Dog("Rex", "Golden Retriever")
14print(rex) # → Dog(name=Rex, breed=Golden Retriever)
15
16# What super() actually resolves to
17print(Dog.__mro__)
18# → (<class 'Dog'>, <class 'Animal'>, <class 'object'>)

best practice

Always use super().__init__(...) in child classes to ensure proper initialisation in cooperative inheritance hierarchies. Avoid calling the parent class by name directly.
Method Overriding

A child class can override any method from its parent by defining a method with the same name. The overridden method can optionally call the parent version via super().

override.py
Python
1class Worker:
2 def work(self) -> str:
3 return "Working..."
4
5 def break_time(self) -> str:
6 return "Taking a break."
7
8class Developer(Worker):
9 def work(self) -> str:
10 # extend parent behaviour
11 base = super().work()
12 return f"{base} Writing code."
13
14 def debug(self) -> str:
15 return "Fixing bugs."
16
17dev = Developer()
18print(dev.work()) # → Working... Writing code.
19print(dev.break_time()) # → Taking a break. (inherited)
20print(dev.debug()) # → Fixing bugs. (new method)
21
22# isinstance / issubclass
23print(issubclass(Developer, Worker)) # True
24print(issubclass(Worker, object)) # True

Python does not have true method overloading (multiple methods with the same name but different signatures). The last definition wins. Use default arguments or *args / **kwargs instead.

Multiple Inheritance

A class can inherit from multiple parent classes. This gives great flexibility but introduces complexity when two parents define the same method.

multi.py
Python
1class Flyer:
2 def move(self) -> str:
3 return "Flying through the air."
4
5 def fuel_type(self) -> str:
6 return "Aviation fuel."
7
8class Swimmer:
9 def move(self) -> str:
10 return "Swimming through water."
11
12 def fuel_type(self) -> str:
13 return "Fish and plankton."
14
15class Duck(Flyer, Swimmer):
16 pass
17
18duck = Duck()
19print(duck.move()) # → Flying through the air.
20print(duck.fuel_type()) # → Aviation fuel.
21
22# The first parent in the list wins by default
23print(Duck.__mro__)
24# → Duck -> Flyer -> Swimmer -> object

warning

Method resolution favours the leftmost parent class listed. If Duck(Swimmer, Flyer) were declared, move() would return "Swimming through water." instead.
Diamond Problem & C3 Linearization

The diamond problem arises when a class inherits from two classes that share a common ancestor. Python resolves this using the C3 linearization algorithm, which produces a consistent, monotonic MRO.

diamond.py
Python
1class A:
2 def whoami(self) -> str:
3 return "A"
4
5class B(A):
6 def whoami(self) -> str:
7 return "B"
8
9class C(A):
10 def whoami(self) -> str:
11 return "C"
12
13class D(B, C):
14 pass
15
16d = D()
17print(d.whoami()) # → B
18
19# MRO: D -> B -> C -> A -> object
20print(D.__mro__)
21
22# The C3 algorithm guarantees:
23# 1. Children come before parents
24# 2. Order of bases in class declaration is preserved
25# 3. Monotonicity — consistent across subclasses

Access D.__mro__ or call help(D) to inspect the resolution order at runtime. The super() proxy follows this order, which makes cooperative multiple inheritance predictable.

cooperative.py
Python
1# Cooperative diamond with super()
2class A:
3 def __init__(self):
4 print("A.__init__")
5
6class B(A):
7 def __init__(self):
8 print("B.__init__")
9 super().__init__()
10
11class C(A):
12 def __init__(self):
13 print("C.__init__")
14 super().__init__()
15
16class D(B, C):
17 def __init__(self):
18 print("D.__init__")
19 super().__init__()
20
21D()
22# Output:
23# D.__init__
24# B.__init__
25# C.__init__
26# A.__init__

info

The output order matches D.__mro__: D -> B -> C -> A -> object. Each super() call passes control to the next class in the chain, not the parent class directly.
Mixins

A mixin is a class that provides reusable behaviour but is not meant to stand alone. Mixins follow naming conventions (often suffixed with Mixin) and work via multiple inheritance to compose functionality.

mixin.py
Python
1class LogMixin:
2 """Add logging capability to any class."""
3 def log(self, message: str) -> None:
4 print(f"[{self.__class__.__name__}] {message}")
5
6class SerializeMixin:
7 """Add JSON serialisation to any class."""
8 def to_dict(self) -> dict:
9 return self.__dict__
10
11 def to_json(self) -> str:
12 import json
13 return json.dumps(self.to_dict())
14
15class User(LogMixin, SerializeMixin):
16 def __init__(self, name: str, email: str):
17 self.name = name
18 self.email = email
19 self.log(f"User created: {name}")
20
21user = User("Alice", "alice@example.com")
22print(user.to_json())
23# → {"name": "Alice", "email": "alice@example.com"}
24
25# Mixins should be small, focused, and stateless

best practice

Mixins should not define __init__ or store instance state. They should be kept small and focused on a single behaviour. Use the Mixin suffix to distinguish them from primary base classes.
Abstract Base Classes (ABC)

Abstract Base Classes define an interface that subclasses must implement. Python provides the abc module with ABC and abstractmethod to enforce contracts at instantiation time.

abc.py
Python
1from abc import ABC, abstractmethod
2
3class Shape(ABC):
4 """Abstract base class for all shapes."""
5
6 @abstractmethod
7 def area(self) -> float:
8 """Calculate area — must be implemented by subclass."""
9 ...
10
11 @abstractmethod
12 def perimeter(self) -> float:
13 ...
14
15 def describe(self) -> str:
16 """Concrete method — available to all subclasses."""
17 return f"{self.__class__.__name__} — area: {self.area():.2f}"
18
19class Circle(Shape):
20 def __init__(self, radius: float):
21 self.radius = radius
22
23 def area(self) -> float:
24 return 3.14159 * self.radius ** 2
25
26 def perimeter(self) -> float:
27 return 2 * 3.14159 * self.radius
28
29c = Circle(5)
30print(c.describe()) # → Circle — area: 78.54
31
32# Shape() would raise TypeError: Can't instantiate abstract class

Static methods, class methods, and properties can also be declared abstract:

abstract-patterns.py
Python
1from abc import ABC, abstractmethod, abstractproperty
2from typing import final
3
4class Document(ABC):
5 @abstractmethod
6 def render(self) -> str:
7 ...
8
9 @abstractmethod
10 @classmethod
11 def from_file(cls, path: str) -> "Document":
12 """Factory — must be implemented by subclass."""
13 ...
14
15 @abstractmethod
16 @staticmethod
17 def supported_extensions() -> set[str]:
18 ...
19
20 @property
21 @abstractmethod
22 def file_size(self) -> int:
23 """Subclasses must provide this property."""
24 ...
25
26class PDFDocument(Document):
27 def __init__(self, path: str, size: int):
28 self.path = path
29 self._size = size
30
31 def render(self) -> str:
32 return f"Rendering PDF: {self.path}"
33
34 @classmethod
35 def from_file(cls, path: str) -> "PDFDocument":
36 return cls(path, 1024)
37
38 @staticmethod
39 def supported_extensions() -> set[str]:
40 return {".pdf"}
41
42 @property
43 def file_size(self) -> int:
44 return self._size

info

Use @final from typing (Python 3.8+) to prevent further overriding. Python's ABC mechanism only enforces that abstract methods are implemented at instantiation time, not at import time.
Composition over Inheritance

Composition — building classes by embedding objects rather than inheriting from them — often leads to more flexible, testable designs. The Gang of Four principle "favour composition over inheritance" remains relevant in Python.

composition.py
Python
1# Inheritance approach — rigid
2class EmailNotifier:
3 def send(self, msg: str):
4 print(f"Sending email: {msg}")
5
6class UserWithEmail(EmailNotifier):
7 def __init__(self, name: str):
8 self.name = name
9 self.send(f"Welcome {name}!")
10
11# Composition approach — flexible
12class Notifier:
13 def __init__(self, channels: list):
14 self.channels = channels
15
16 def send(self, message: str):
17 for channel in self.channels:
18 channel.send(message)
19
20class EmailChannel:
21 def send(self, msg: str):
22 print(f"Email: {msg}")
23
24class SMSChannel:
25 def send(self, msg: str):
26 print(f"SMS: {msg}")
27
28class User:
29 def __init__(self, name: str, notifier: Notifier):
30 self.name = name
31 self.notifier = notifier
32
33 def welcome(self):
34 self.notifier.send(f"Welcome {self.name}!")
35
36# Compose different behaviours at runtime
37n = Notifier([EmailChannel(), SMSChannel()])
38u = User("Alice", n)
39u.welcome()
40# → Email: Welcome Alice!
41# → SMS: Welcome Alice!

Use inheritance for is-a relationships (a Dog is an Animal). Use composition for has-a relationships (a User has a Notifier). Composition supports dependency injection, easier testing, and runtime reconfiguration.

best practice

Prefer composition when: the relationship is behavioural rather than taxonomic, you need to swap implementations at runtime, or the subclass would override most parent methods. Reserve inheritance for genuinely hierarchical domains.
isinstance.py
Python
1# isinstance / issubclass — runtime type checks
2from collections.abc import Iterable, Mapping
3
4class CustomList:
5 def __getitem__(self, index):
6 return index
7 def __len__(self):
8 return 0
9
10# Register as virtual subclass without inheritance
11Iterable.register(CustomList)
12
13print(isinstance(CustomList(), Iterable)) # True
14print(issubclass(CustomList, Iterable)) # True
15
16# Virtual subclass registration works with ABCs
17# but does NOT affect MRO or super()
Best Practices

1. Keep inheritance hierarchies shallow (ideally no deeper than 2-3 levels). Deep hierarchies are hard to debug and refactor.

2. Use super() consistently — never bypass it by calling parent constructors directly. This ensures cooperative inheritance works correctly.

3. Prefer ABCs and @abstractmethod over duck-typing-only contracts when you need to enforce an interface across a codebase.

4. Use mixins for reusable, orthogonal behaviour. Keep them stateless and avoid __init__ in mixin classes.

5. Favour composition over inheritance for most real-world scenarios. Ask "has-a?" before "is-a?".

6. Understand self.__class__ vs type(self). In inheritance chains, self.__class__ always refers to the actual runtime class of the instance.

7. Use @staticmethod when the method does not depend on class or instance state; use @classmethod for polymorphic factory methods. Both are inherited normally but are not overridden in the traditional sense.

inheritance-patterns.py
Python
1# Static method vs class method in inheritance
2class Base:
3 @staticmethod
4 def info() -> str:
5 return "Base info"
6
7 @classmethod
8 def create(cls) -> "Base":
9 return cls()
10
11class Derived(Base):
12 @staticmethod
13 def info() -> str:
14 return "Derived info"
15
16 # classmethod create is inherited — cls will be Derived
17 pass
18
19print(Base.info()) # → Base info
20print(Derived.info()) # → Derived info
21print(Derived.create()) # → <Derived object> (cls=Derived)
22
23# Property inheritance
24class Rectangle:
25 def __init__(self, w: float, h: float):
26 self._w = w
27 self._h = h
28
29 @property
30 def area(self) -> float:
31 return self._w * self._h
32
33class Square(Rectangle):
34 def __init__(self, side: float):
35 super().__init__(side, side)
36
37 @property
38 def area(self) -> float:
39 # Override property behaviour
40 return f"Square area: {self._w * self._h}"
41
42sq = Square(4)
43print(sq.area) # → Square area: 16
$Blueprint — Engineering Documentation·Section ID: PYTHON-INH·Revision: 1.0