Inheritance in Python

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:

Avatar photo
Great Learning Editorial Team
The Great Learning Editorial Staff includes a dynamic team of subject matter experts, instructors, and education professionals who combine their deep industry knowledge with innovative teaching methods. Their mission is to provide learners with the skills and insights needed to excel in their careers, whether through upskilling, reskilling, or transitioning into new fields.
Scroll to Top