Inheritance is a way to form relationships between classes. You have a “parent” class (also called a base class or superclass) and a “child” class (a derived class or subclass). The child class gets all the methods and properties of the parent class. That’s it. It’s a mechanism for code reuse. Don’t repeat yourself (DRY) is a core programming principle, and inheritance is one way to achieve it.
Why use it?
Imagine you’re coding a game. You have Enemy objects. Goblins, Orcs, and Dragons are all enemies. They all have health, can take damage, and can attack. Instead of writing that code three times, you create a base Enemy class with that shared logic. Then, your Goblin, Orc, and Dragon classes can inherit from Enemy. They get all the Enemy functionality for free, and you can add specific things to each, like a Dragon’s breathe_fire()
method.
Here’s the basic syntax. No magic here.
# Parent class
class Enemy:
def __init__(self, name, health):
self.name = name
self.health = health
def take_damage(self, amount):
self.health -= amount
print(f"{self.name} takes {amount} damage, {self.health} HP left.")
# Child class
class Goblin(Enemy):
# This class is empty, but it already has everything from Enemy
pass
# Let's use it
grog = Goblin("Grog the Goblin", 50)
grog.take_damage(10) # This method comes from the Enemy class
# Output: Grog the Goblin takes 10 damage, 40 HP left.
Types of Inheritance
There are a few ways to structure this parent-child relationship.
1. Single Inheritance
This is what you just saw. One child class inherits from one parent class. It’s the simplest and most common form of inheritance. Square
inherits from Rectangle
, Rectangle
inherits from Shape
. Clean and linear.
2. Multilevel Inheritance
This is just a chain of single inheritance. A is the grandparent, B is the parent, and C is the child. C inherits from B, and B inherits from A. This means C gets all the methods and properties from both B and A.
class Organism:
def breathe(self):
print("Inhale, exhale.")
class Animal(Organism):
def move(self):
print("Moving around.")
class Dog(Animal):
def bark(self):
print("Woof!")
my_dog = Dog()
my_dog.bark() # From Dog
my_dog.move() # From Animal
my_dog.breathe() # From Organism
This can get messy if the chain is too long. Deep inheritance hierarchies are often a sign of bad design.
3. Multiple Inheritance
This is where a single child class inherits from multiple parent classes at the same time. This is where Python gets powerful, and also dangerous.
class Flyer:
def fly(self):
print("I am flying.")
class Swimmer:
def swim(self):
print("I am swimming.")
class Duck(Flyer, Swimmer):
def quack(self):
print("Quack!")
donald = Duck()
donald.fly()
donald.swim()
donald.quack()
The Duck
class can both fly and swim because it inherits from both Flyer
and Swimmer
. This sounds great, but it introduces a major problem: What if both parent classes have a method with the same name? This is known as the “Diamond Problem,” and it leads us to the next critical topic.
Method Resolution Order (MRO)
When you call a method on an object from a class that uses multiple inheritance, how does Python know which parent’s method to use? It follows a specific order called the Method Resolution Order (MRO).
The MRO defines the sequence of classes to search when looking for a method. Python uses an algorithm called C3 linearization to figure this out. The key rules are that a child class is checked before its parents, and if there are multiple parents, they are checked in the order you list them in the class definition.
You can see the MRO for any class by using the .mro()
method or the __mro__
attribute.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.mro())
Output:
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
When you call a method on a D
object, Python will look for it in this order: D
, then B
, then C
, then A
, and finally the base object class that everything in Python inherits from. The first place it finds the method, it stops and uses that one. This predictability is crucial for managing complex inheritance structures.
The super() Function: Your Best Friend in Inheritance
When you have a child class, you often want to extend the parent’s method, not completely replace it. For example, in the child’s __init__
, you first need to run the parent’s __init__
to set up all the inherited attributes.
You do this with super()
. The super()
function gives you a way to call the parent class’s methods. More accurately, it allows you to call the next method in the MRO chain.
Let’s fix our Enemy
example to add a Goblin-specific attribute.
class Enemy:
def __init__(self, name, health):
self.name = name
self.health = health
class Goblin(Enemy):
def __init__(self, name, health, has_club):
# Call the parent's __init__ to handle name and health
super().__init__(name, health)
# Now add the child-specific attribute
self.has_club = has_club
grog = Goblin("Grog", 50, True)
print(grog.name) # Output: Grog
print(grog.has_club) # Output: True
Without super().__init__(name, health)
, the grog
object would never get its .name
or .health
attributes because the Enemy.__init__
would never be called. You replaced it, but you didn’t extend it. super()
solves this.
super()
is essential for making multiple inheritance work properly. If you call parent methods directly by name (e.g., B.__init__(self, ...)
), you can end up calling the same method from a common ancestor multiple times, which leads to bugs. super()
respects the MRO and ensures each method in the inheritance chain is called only once.
Is Inheritance Always the Answer? (No.)
Inheritance is a powerful tool, but it’s often overused by beginners. It creates a tight coupling between your classes. A change in the parent can break all the children.
The main alternative is Composition. Instead of a class being something (inheritance), it has something (composition).
Let’s say you have a Car
class. You could have it inherit from a Vehicle
class. But what about its engine? A Car
isn’t an Engine
, a Car
has an Engine
. So you would create a separate Engine
class and give your Car
an engine
attribute that is an instance of the Engine
class.
class Engine:
def start(self):
print("Engine starts.")
class Car:
def __init__(self, make):
self.make = make
self.engine = Engine() # Composition: Car HAS AN Engine
def drive(self):
self.engine.start()
print(f"The {self.make} is driving.")
my_car = Car("Ford")
my_car.drive()
This is often more flexible. You can easily swap out the Engine
object for a different one (e.g., ElectricEngine
) without changing the Car
class itself. The general rule is to “favor composition over inheritance.” If the relationship is not a clear “is-a” relationship (a Goblin is an Enemy), composition is probably the better design choice.
Also Read: