Codetail

Article 4 of 8

Inheritance

IS-A relationships, super(), and when to walk away.

22 min read

Inheritance models IS-A relationships

Before reaching for inheritance, ask one question: is this actually an IS-A relationship? A Dog IS-A Animal. A Car IS-A Vehicle. A SavingsAccount IS-A BankAccount. When you can say that cleanly, inheritance is probably the right tool.

The subclass inherits everything from the parent. Every method, every attribute. It can add new ones, and it can replace (override) ones that need different behavior.

Python
1class Animal:
2 def __init__(self, name):
3 self.name = name
4
5 def speak(self):
6 raise NotImplementedError("subclasses must implement speak()")
7
8 def describe(self):
9 return f"I am {self.name}"
10
11class Dog(Animal):
12 def speak(self):
13 return f"{self.name} says: Woof!"
14
15class Cat(Animal):
16 def speak(self):
17 return f"{self.name} says: Meow."
18
19class Duck(Animal):
20 def speak(self):
21 return f"{self.name} says: Quack."
22
23animals = [Dog("Rex"), Cat("Luna"), Duck("Donald")]
24for animal in animals:
25 print(animal.speak())
26 print(animal.describe()) # inherited from Animal, works on all

Every animal shares the describe() method, which was written once on the parent class. Each has its own speak() that overrides the parent's placeholder. This is inheritance doing what it is supposed to do: share common code, allow specialization where needed.

super(): calling the parent without hardcoding it

When you override a method in a subclass, you often still want the parent's version to run. The naive approach is to hardcode the parent class name:

Python
1class Animal:
2 def __init__(self, name):
3 self.name = name
4
5class Dog(Animal):
6 def __init__(self, name, breed):
7 Animal.__init__(self, name) # works, but brittle
8 self.breed = breed

This breaks the moment you restructure the hierarchy. If Dog stops inheriting from Animal directly, every hardcoded reference breaks. Use super() instead:

Python
1class Animal:
2 def __init__(self, name):
3 self.name = name
4 print(f"Animal.__init__ called for {name}")
5
6class Dog(Animal):
7 def __init__(self, name, breed):
8 super().__init__(name) # calls Animal.__init__, passes self automatically
9 self.breed = breed
10 print(f"Dog.__init__ added breed={breed}")
11
12class GuideDog(Dog):
13 def __init__(self, name, breed, handler):
14 super().__init__(name, breed) # calls Dog.__init__
15 self.handler = handler
16 print(f"GuideDog.__init__ added handler={handler}")
17
18g = GuideDog("Rex", "Labrador", "Alice")

Each class calls super().__init__() and the chain runs upward. You never hardcode a class name. If you restructure the hierarchy, the calls still work correctly.

Rule: whenever you override __init__, call super().__init__() first, before doing any of your own setup. This ensures the parent's state is initialized before you build on top of it.

MRO: the order Python searches for methods

When you call a method on an object, Python searches through a list of classes to find it. That list is the Method Resolution Order (MRO). For simple single inheritance it is obvious: check the instance's class, then the parent, then the parent's parent, up to object.

Python
1class Animal:
2 def speak(self):
3 return "..."
4
5class Dog(Animal):
6 def speak(self):
7 return "Woof"
8
9class GuideDog(Dog):
10 pass # no speak() override
11
12g = GuideDog("Rex", "Labrador", "Alice")
13print(GuideDog.__mro__)

When you call g.speak(), Python checks GuideDog first (no speak), then Dog (found it), and uses that. The Animal version is never reached because Dog already has it.

Multiple inheritance and the diamond problem

Python allows a class to inherit from more than one parent. This creates ambiguity when both parents define the same method. The MRO resolves this unambiguously using an algorithm called C3 linearization.

Python
1class A:
2 def hello(self):
3 return "A"
4
5class B(A):
6 def hello(self):
7 return "B"
8
9class C(A):
10 def hello(self):
11 return "C"
12
13class D(B, C):
14 pass
15
16d = D()
17print(d.hello()) # B -- not A, not C
18print(D.__mro__)

The MRO for D is D, B, C, A, object. When hello() is called, Python finds it in B first and stops there. The order of parent classes in the class definition (D(B, C)) determines the search order.

In practice, multiple inheritance is rarely needed and often a sign that composition would be a better fit. Use it sparingly, and when you do, keep the MRO in mind.

When not to use inheritance

There is a principle called the Liskov Substitution Principle. In plain English: if Dog IS-A Animal, then anywhere you use an Animal, a Dog should work without anything breaking. If that is not true, the inheritance is wrong.

Python
1class Rectangle:
2 def __init__(self, width, height):
3 self.width = width
4 self.height = height
5
6 def area(self):
7 return self.width * self.height
8
9class Square(Rectangle):
10 def __init__(self, side):
11 super().__init__(side, side)
12
13 # Problem: if someone sets width on a Square, height stays the same
14 # A square is no longer a square.
15
16s = Square(5)
17s.width = 10 # now s.height is still 5
18print(s.area()) # 50 -- but a 10x5 thing is not a square

A square IS-A rectangle mathematically, but in code, making Square inherit from Rectangle creates a footgun. Code that works with rectangles might adjust width and height independently, which breaks the square constraint.

Inherit for IS-A, compose for HAS-A

Another common mistake is inheriting just to reuse code:

Python
1# Inheriting for code reuse, not IS-A
2class Logger:
3 def log(self, msg):
4 print(f"[LOG] {msg}")
5
6class UserService(Logger): # UserService IS-A Logger? No.
7 def create_user(self, name):
8 self.log(f"Creating user: {name}")
9 # ... actual user creation
10
11# Better: HAS-A Logger
12class UserService:
13 def __init__(self):
14 self._logger = Logger()
15
16 def create_user(self, name):
17 self._logger.log(f"Creating user: {name}")
18 # ... actual user creation

A UserService is not a kind of Logger. It uses a logger. When the relationship is HAS-A, the right pattern is to hold the dependency as an attribute, not to inherit from it. This is composition, covered in the last article of this series.

When in doubt, ask the IS-A question. If you cannot say "X is a kind of Y" naturally, do not use inheritance.