Inheritance & Polymorphism

0/5 in this phase0/54 across the roadmap

📖 Concept

Python supports single inheritance, multiple inheritance, and abstract base classes. The Method Resolution Order (MRO) determines which method gets called in a class hierarchy.

Inheritance types:

  • Single inheritance: class Child(Parent) — one parent class
  • Multiple inheritance: class Child(Parent1, Parent2) — multiple parents
  • Multilevel inheritance: class C(B), class B(A) — chain of parents

The super() function: super() calls methods from the parent class following the MRO. It's essential for cooperative multiple inheritance — each class calls super() to ensure all parents' methods run.

Method Resolution Order (MRO): Python uses the C3 linearization algorithm to determine the order in which classes are searched for methods. You can inspect it with ClassName.__mro__ or ClassName.mro().

Abstract Base Classes (ABCs): ABCs define interfaces — they declare methods that subclasses MUST implement. Use the abc module:

from abc import ABC, abstractmethod
class Shape(ABC):
    @abstractmethod
    def area(self): pass

You cannot instantiate an ABC directly — subclasses must implement all abstract methods.

Duck typing is Python's preferred polymorphism style: "If it walks like a duck and quacks like a duck, it's a duck." Instead of checking types, check for capabilities (methods/attributes).

💻 Code Example

codeTap to expand ⛶
1# ============================================================
2# Single inheritance
3# ============================================================
4class Animal:
5 def __init__(self, name: str, sound: str):
6 self.name = name
7 self.sound = sound
8
9 def speak(self) -> str:
10 return f"{self.name} says {self.sound}!"
11
12 def __str__(self) -> str:
13 return f"{self.__class__.__name__}({self.name})"
14
15class Dog(Animal):
16 def __init__(self, name: str, breed: str):
17 super().__init__(name, "Woof") # Call parent's __init__
18 self.breed = breed
19
20 def fetch(self, item: str) -> str:
21 return f"{self.name} fetches the {item}!"
22
23class Cat(Animal):
24 def __init__(self, name: str):
25 super().__init__(name, "Meow")
26
27 def speak(self) -> str:
28 # Override parent method
29 return f"{self.name} purrs softly... then says {self.sound}!"
30
31# Polymorphism — same interface, different behavior
32animals = [Dog("Rex", "German Shepherd"), Cat("Whiskers")]
33for animal in animals:
34 print(animal.speak())
35 # Rex says Woof!
36 # Whiskers purrs softly... then says Meow!
37
38# isinstance and issubclass
39print(isinstance(animals[0], Dog)) # True
40print(isinstance(animals[0], Animal)) # True (inheritance chain)
41print(issubclass(Dog, Animal)) # True
42
43# ============================================================
44# Multiple inheritance and MRO
45# ============================================================
46class Flyable:
47 def fly(self):
48 return f"{self.name} is flying!"
49
50class Swimmable:
51 def swim(self):
52 return f"{self.name} is swimming!"
53
54class Duck(Animal, Flyable, Swimmable):
55 def __init__(self, name):
56 super().__init__(name, "Quack")
57
58donald = Duck("Donald")
59print(donald.speak()) # Donald says Quack!
60print(donald.fly()) # Donald is flying!
61print(donald.swim()) # Donald is swimming!
62
63# Inspect MRO
64print(Duck.__mro__)
65# (Duck, Animal, Flyable, Swimmable, object)
66
67# ============================================================
68# The diamond problem and super()
69# ============================================================
70class Base:
71 def __init__(self):
72 print("Base.__init__")
73
74class Left(Base):
75 def __init__(self):
76 print("Left.__init__")
77 super().__init__() # Calls Right.__init__ (not Base!)
78
79class Right(Base):
80 def __init__(self):
81 print("Right.__init__")
82 super().__init__() # Calls Base.__init__
83
84class Diamond(Left, Right):
85 def __init__(self):
86 print("Diamond.__init__")
87 super().__init__() # Calls Left.__init__
88
89# d = Diamond()
90# Output:
91# Diamond.__init__
92# Left.__init__
93# Right.__init__super() in Left calls Right, not Base!
94# Base.__init__Base is called only ONCE
95
96# MRO: DiamondLeftRightBase → object
97
98# ============================================================
99# Abstract Base Classes
100# ============================================================
101from abc import ABC, abstractmethod
102
103class Shape(ABC):
104 """Abstract base class for shapes."""
105
106 @abstractmethod
107 def area(self) -> float:
108 """Calculate the area."""
109 pass
110
111 @abstractmethod
112 def perimeter(self) -> float:
113 """Calculate the perimeter."""
114 pass
115
116 def describe(self) -> str:
117 """Concrete method using abstract methods."""
118 return f"{self.__class__.__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"
119
120# shape = Shape() # TypeError: Can't instantiate abstract class
121
122class Circle(Shape):
123 def __init__(self, radius: float):
124 self.radius = radius
125
126 def area(self) -> float:
127 import math
128 return math.pi * self.radius ** 2
129
130 def perimeter(self) -> float:
131 import math
132 return 2 * math.pi * self.radius
133
134class Rectangle(Shape):
135 def __init__(self, width: float, height: float):
136 self.width = width
137 self.height = height
138
139 def area(self) -> float:
140 return self.width * self.height
141
142 def perimeter(self) -> float:
143 return 2 * (self.width + self.height)
144
145# Polymorphism with abstract base class
146shapes = [Circle(5), Rectangle(4, 6)]
147for shape in shapes:
148 print(shape.describe())
149 # Circle: area=78.54, perimeter=31.42
150 # Rectangle: area=24.00, perimeter=20.00
151
152# ============================================================
153# Duck typing — Pythonic polymorphism
154# ============================================================
155class FileWriter:
156 def write(self, data):
157 with open("output.txt", "a") as f:
158 f.write(data)
159
160class ConsoleWriter:
161 def write(self, data):
162 print(data)
163
164class NetworkWriter:
165 def write(self, data):
166 # Simulate sending data over network
167 print(f"Sending: {data}")
168
169def save_report(writer, data):
170 """Works with ANY object that has a write() method."""
171 writer.write(data) # Duck typing — no type check needed
172
173# All three work without a shared base class
174save_report(ConsoleWriter(), "Hello")
175save_report(NetworkWriter(), "Hello")
176
177# Check capabilities instead of types
178def safe_write(writer, data):
179 if hasattr(writer, 'write') and callable(writer.write):
180 writer.write(data)
181 else:
182 raise TypeError(f"{type(writer).__name__} doesn't support write()")

🏋️ Practice Exercise

Exercises:

  1. Create a class hierarchy: Vehicle (base) → Car, Motorcycle, Truck. Each has fuel_efficiency() and __str__. Write a function that calculates total fuel cost for a fleet using polymorphism.

  2. Implement the diamond problem: create classes A, B(A), C(A), D(B, C) with a method greet() in each. Trace the MRO and explain the output when D().greet() is called with super().

  3. Create an ABC Database with abstract methods connect(), query(), close(). Implement SQLiteDB, PostgresDB, and MockDB subclasses.

  4. Demonstrate duck typing: create three unrelated classes that all have a serialize() method. Write a generic export(items) function that calls serialize() on each.

  5. Implement a Mixin pattern: create JSONSerializableMixin, LoggableMixin, and ValidatableMixin. Show how a class can use multiple mixins without deep inheritance.

  6. Create a Plugin system using ABCs: define a PluginBase ABC, implement several plugins, and write a loader that discovers and instantiates all plugins.

⚠️ Common Mistakes

  • Forgetting to call super().__init__() in subclass constructors — parent attributes won't be initialized, causing AttributeError later.

  • Using deep inheritance hierarchies (more than 3-4 levels). Prefer composition over inheritance: class Car: engine = Engine() instead of class Car(Engine).

  • Not understanding MRO in multiple inheritance — super() doesn't always call the immediate parent. It follows the MRO, which can be surprising in diamond inheritance.

  • Checking types with type(obj) == SomeClass instead of isinstance(obj, SomeClass). The former doesn't work with inheritance; the latter checks the entire hierarchy.

  • Using abstract base classes when duck typing would suffice. Python's philosophy is 'ask for forgiveness, not permission' — try the operation, handle the exception if it fails (EAFP).

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Inheritance & Polymorphism