Inheritance and Multiple Inheritance#
Inheritance lets a class reuse and extend another class's behavior — an is-a relationship (Dog is an Animal). Multiple inheritance lets a class inherit from more than one parent — powerful for mixins, dangerous in deep hierarchies. Python supports both fully, with C3 linearization determining method lookup order.
This page is the advanced companion to Introdicion to OOPs and Classes and OOP Basics.
How to use this page
Master single inheritance and super() first, then mixins and diamonds. Essential follow-up: Method Resolution Order, Polymorphism.
At a glance
| Track | Python Advanced → Object Oriented Programming |
| Sections | 15 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: Single inheritance fundamentals · super() — not "call the parent" · Method overriding vs overloading · isinstance and issubclass · Composition vs inheritance · Multiple inheritance · Mixins — intended multiple inheritance · The diamond problem · … (+7 more)
- Single inheritance fundamentals
super()— not "call the parent"- Method overriding vs overloading
isinstanceandissubclass- Composition vs inheritance
- Multiple inheritance
- Mixins — intended multiple inheritance
- The diamond problem
- Cooperative multiple inheritance
- Template method pattern
- Inheriting built-in types
__init_subclass__— hook on subclass creation … and 3 more sections
Single inheritance fundamentals#
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "woof"
class GuideDog(Dog):
def __init__(self, name: str, owner: str):
super().__init__(name)
self.owner = owner
| Term | Meaning |
|---|---|
| Base / parent / superclass | Class being inherited from |
| Derived / child / subclass | Class that inherits |
| Override | Redefine a method in subclass |
| Extend | Add new methods or call super() then add behavior |
super() — not "call the parent"#
super() returns a proxy that delegates to the next class in the MRO — not necessarily the direct parent:
class A:
def greet(self):
return "A"
class B(A):
def greet(self):
return super().greet() + " B"
class C(A):
def greet(self):
return super().greet() + " C"
class D(B, C):
def greet(self):
return super().greet() + " D"
D().greet() # "A C B D"
The order follows D.__mro__: (D, B, C, A, object).
Interview trap — super() in single inheritance
class Parent:
def __init__(self):
self.initialized = True
class Child(Parent):
def __init__(self, x: int):
super().__init__() # required — Parent.__init__ not automatic
self.x = x
__init__. Forgetting super().__init__() leaves parent state uninitialized.
Method overriding vs overloading#
Python has override (same name, subclass replaces) but no method overloading by signature:
class Calculator:
def add(self, a: int, b: int = 0) -> int:
return a + b
# Can't define add(self, a, b, c) as separate overload — second replaces first
Use default arguments, *args, or @overload from typing for type checkers only:
from typing import overload
class Converter:
@overload
def convert(self, x: int) -> str: ...
@overload
def convert(self, x: str) -> int: ...
def convert(self, x: int | str) -> int | str:
if isinstance(x, int):
return str(x)
return int(x)
isinstance and issubclass#
dog = Dog("Rex")
isinstance(dog, Dog) # True
isinstance(dog, Animal) # True — transitive
issubclass(GuideDog, Dog) # True
issubclass(Dog, GuideDog) # False
Prefer isinstance(obj, BaseClass) over type(obj) == BaseClass — respects subclasses.
Composition vs inheritance#
| Relationship | Pattern | When |
|---|---|---|
| is-a | Inheritance | Subtype substitutability is real |
| has-a | Composition | Combining behaviors, avoiding fragile hierarchies |
# Inheritance — genuine subtype
class SortedList(list):
def append(self, item):
super().append(item)
self.sort()
# Composition — preferred for flexibility
class Engine:
def start(self) -> None: ...
class Car:
def __init__(self):
self.engine = Engine()
def start(self) -> None:
self.engine.start()
Rule of thumb: if you're inheriting only to reuse code (not for polymorphic substitution), consider composition.
Multiple inheritance#
class Flyer:
def fly(self) -> str:
return "flying"
class Swimmer:
def swim(self) -> str:
return "swimming"
class Duck(Animal, Flyer, Swimmer):
def speak(self) -> str:
return "quack"
d = Duck("Donald")
d.speak() # quack
d.fly() # flying
d.swim() # swimming
Python resolves d.fly() by walking the MRO left-to-right through base classes.
Mixins — intended multiple inheritance#
Mixins are small, focused classes that add one capability — not meant to stand alone:
class JSONMixin:
def to_json(self) -> str:
import json
return json.dumps(self.__dict__)
class TimestampMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
import time
self.created_at = time.time()
class Record(JSONMixin, TimestampMixin):
def __init__(self, name: str):
super().__init__()
self.name = name
Record("Alice").to_json()
Mixin rules#
| Do | Don't |
|---|---|
| Keep mixins small and focused | Create deep mixin chains |
Call super().__init__() cooperatively |
Assume a specific parent __init__ |
| Put mixins before base class in MRO | Use mixins for unrelated state blobs |
Name with Mixin suffix |
Instantiate mixins directly |
The diamond problem#
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
super().method()
class C(A):
def method(self):
print("C")
super().method()
class D(B, C):
def method(self):
print("D")
super().method()
D().method()
# Output:
# D
# B
# C
# A
C3 linearization produces D → B → C → A → object — each class appears once, parents respect order. Details: Method Resolution Order.
Cooperative multiple inheritance#
Every class in the chain must call super() — not hardcode a parent class name:
# Bad — breaks if MRO changes
class B(A):
def __init__(self):
A.__init__(self)
# Good — cooperative
class B(A):
def __init__(self, **kwargs):
super().__init__(**kwargs)
Full cooperative example#
class Root:
def __init__(self, value: int, **kwargs):
self.value = value
class Left(Root):
def __init__(self, value: int, left_tag: str = "", **kwargs):
super().__init__(value, **kwargs)
self.left_tag = left_tag
class Right(Root):
def __init__(self, value: int, right_tag: str = "", **kwargs):
super().__init__(value, **kwargs)
self.right_tag = right_tag
class Both(Left, Right):
def __init__(self, value: int, left_tag: str, right_tag: str):
super().__init__(value, left_tag=left_tag, right_tag=right_tag)
b = Both(1, "L", "R")
b.value # 1
b.left_tag # "L"
b.right_tag # "R"
Passing kwargs through the chain requires each class to accept and forward unknown kwargs — or use a shared initialization protocol.
Template method pattern#
Base class defines algorithm skeleton; subclasses fill in steps:
class DataProcessor:
def process(self, raw: str) -> dict:
cleaned = self.clean(raw)
parsed = self.parse(cleaned)
return self.validate(parsed)
def clean(self, raw: str) -> str:
raise NotImplementedError
def parse(self, cleaned: str) -> dict:
raise NotImplementedError
def validate(self, parsed: dict) -> dict:
return parsed # optional override
class CSVProcessor(DataProcessor):
def clean(self, raw: str) -> str:
return raw.strip()
def parse(self, cleaned: str) -> dict:
keys, *rows = cleaned.split("\n")
headers = keys.split(",")
return dict(zip(headers, rows[0].split(",")))
ABC version enforces steps at instantiation: Abstract Base Classes.
Inheriting built-in types#
class CounterDict(dict):
def __init__(self):
super().__init__()
self.access_count = 0
def __getitem__(self, key):
self.access_count += 1
return super().__getitem__(key)
Caution: built-in types (dict, list, str) are implemented in C — overriding may not intercept all code paths. For full control, wrap (composition) or use collections.UserDict.
__init_subclass__ — hook on subclass creation#
class PluginBase:
plugins: dict[str, type] = {}
def __init_subclass__(cls, name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if name:
PluginBase.plugins[name] = cls
class EmailPlugin(PluginBase, name="email"):
pass
class SMSPlugin(PluginBase, name="sms"):
pass
PluginBase.plugins # {"email": EmailPlugin, "sms": SMSPlugin}
Useful for registration patterns without metaclasses.
When not to inherit#
| Situation | Better approach |
|---|---|
| Reusing one utility method | Composition or module function |
| "Extending" unrelated class for one feature | Wrapper |
| Deep tree (>3 levels) | Flatten, use composition |
| Changing built-in semantics heavily | Don't subclass list/dict casually |
| Cross-cutting concerns (logging, timing) | Decorators |
Inheritance with ABCs#
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Rectangle(Shape):
def __init__(self, w: float, h: float):
self.w, self.h = w, h
def area(self) -> float:
return self.w * self.h
Abstract methods must be implemented in concrete subclasses — see Abstract Base Classes.
Interview patterns#
LeetCode-style tree/list nodes — usually no inheritance#
class TreeNode:
def __init__(self, val: int = 0, left: "TreeNode | None" = None, right: "TreeNode | None" = None):
self.val = val
self.left = left
self.right = right
Design question — exception hierarchy#
class AppError(Exception):
"""Base for all app errors."""
class ValidationError(AppError):
pass
class NotFoundError(AppError):
pass
Exception hierarchies are one of the best uses of single inheritance in Python.
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
Forgetting super().__init__() |
Parent state uninitialized | Always call super in __init__ |
| Hardcoding parent class in MRO | Breaks with multiple inheritance | Use super() |
| Deep inheritance | Fragile, hard to debug | Composition + shallow trees |
Mixin without super() |
Skips other mixin init | Cooperative super().__init__ |
Subclassing list/dict casually |
Some operations bypass overrides | UserDict, composition |
| Multiple inheritance for code reuse only | Diamond problems, confusion | Composition |
| Wrong base order in mixins | Unexpected MRO | Mixins first, base last |
Mental model checklist#
- What does
super()actually refer to in multiple inheritance? - When is composition preferred over inheritance?
- What is a mixin, and how should it call
super()? - Why doesn't Python auto-call parent
__init__? - How does Python resolve the diamond problem?
What's next#
| Topic | Page |
|---|---|
| MRO deep dive | Method Resolution Order |
| Polymorphism | Polymorphism |
| ABCs | Abstract Base Classes |
| Intermediate OOP | Classes and OOP Basics |