backend / python oop / 06_inheritance_in_python.md

Inheritance in Python

4 interview angles 9 min read source

Inheritance in Python

Inheritance is one of the four fundamental principles of Object-Oriented Programming (OOP). It allows a class (child/derived class) to inherit attributes and methods from another class (parent/base class), promoting code reuse and establishing a hierarchical relationship between classes.

The child class can use all the public and protected attributes and methods of the parent class, and can also override or extend them.


What is Inheritance?

Inheritance enables:

  • Code Reuse: Avoid duplicating code across classes
  • Hierarchy: Establish “is-a” relationships between classes
  • Polymorphism: Use child classes where parent classes are expected
  • Extensibility: Add new functionality to existing classes
class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species

    def speak(self):
        return "Some sound"

    def move(self):
        return f"{self.name} is moving"

    def get_info(self):
        return f"{self.name} is a {self.species}"

class Dog(Animal):  # Dog inherits from Animal
    def __init__(self, name, breed):
        super().__init__(name, "Dog")  # Call parent constructor
        self.breed = breed

    def speak(self):  # Override parent method
        return "Woof!"

    def fetch(self):  # Add new method
        return f"{self.name} is fetching the ball"

class Cat(Animal):  # Cat inherits from Animal
    def __init__(self, name, color):
        super().__init__(name, "Cat")
        self.color = color

    def speak(self):  # Override parent method
        return "Meow!"

    def climb(self):  # Add new method
        return f"{self.name} is climbing a tree"

# Usage
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")

print(dog.get_info())    # Buddy is a Dog
print(dog.speak())       # Woof!
print(dog.fetch())       # Buddy is fetching the ball

print(cat.get_info())    # Whiskers is a Cat
print(cat.speak())       # Meow!
print(cat.climb())       # Whiskers is climbing a tree

Types of Inheritance

1. Single Inheritance

A class inherits from only one parent class:

class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model

    def start_engine(self):
        return f"{self.brand} {self.model} engine started"

    def stop_engine(self):
        return f"{self.brand} {self.model} engine stopped"

class Car(Vehicle):  # Single inheritance
    def __init__(self, brand, model, num_doors):
        super().__init__(brand, model)
        self.num_doors = num_doors

    def open_trunk(self):
        return f"Opening trunk of {self.brand} {self.model}"

car = Car("Toyota", "Camry", 4)
print(car.start_engine())  # Toyota Camry engine started
print(car.open_trunk())    # Opening trunk of Toyota Camry

2. Multiple Inheritance

A class inherits from multiple parent classes:

class Flyable:
    def fly(self):
        return "Flying high in the sky"

    def land(self):
        return "Landing safely"

class Swimmable:
    def swim(self):
        return "Swimming in the water"

    def dive(self):
        return "Diving deep"

class Duck(Flyable, Swimmable):  # Multiple inheritance
    def __init__(self, name):
        self.name = name

    def quack(self):
        return f"{self.name} says Quack!"

duck = Duck("Donald")
print(duck.fly())    # Flying high in the sky
print(duck.swim())   # Swimming in the water
print(duck.quack())  # Donald says Quack!

3. Multilevel Inheritance

A class inherits from a derived class, creating a chain of inheritance:

class Animal:
    def __init__(self, name):
        self.name = name

    def eat(self):
        return f"{self.name} is eating"

class Mammal(Animal):
    def __init__(self, name, has_fur=True):
        super().__init__(name)
        self.has_fur = has_fur

    def give_birth(self):
        return f"{self.name} gives birth to live young"

class Dog(Mammal):
    def __init__(self, name, breed):
        super().__init__(name, has_fur=True)
        self.breed = breed

    def bark(self):
        return f"{self.name} barks loudly"

dog = Dog("Rex", "German Shepherd")
print(dog.eat())         # Rex is eating
print(dog.give_birth())  # Rex gives birth to live young
print(dog.bark())        # Rex barks loudly

4. Hierarchical Inheritance

Multiple classes inherit from the same parent class:

class Shape:
    def __init__(self, color):
        self.color = color

    def get_color(self):
        return self.color

    def area(self):
        pass  # Abstract method

class Circle(Shape):
    def __init__(self, color, radius):
        super().__init__(color)
        self.radius = radius

    def area(self):
        import math
        return math.pi * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Triangle(Shape):
    def __init__(self, color, base, height):
        super().__init__(color)
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

# All inherit from Shape
circle = Circle("Red", 5)
rectangle = Rectangle("Blue", 4, 6)
triangle = Triangle("Green", 3, 8)

print(f"Circle area: {circle.area():.2f}")      # Circle area: 78.54
print(f"Rectangle area: {rectangle.area()}")     # Rectangle area: 24
print(f"Triangle area: {triangle.area()}")       # Triangle area: 12.0

Method Resolution Order (MRO)

MRO determines the order in which Python searches for methods in inheritance hierarchies:

class A:
    def method(self):
        return "A"

class B(A):
    def method(self):
        return "B"

class C(A):
    def method(self):
        return "C"

class D(B, C):
    pass

class E(C, B):
    pass

# Check MRO
print(D.__mro__)  # (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
print(E.__mro__)  # (<class '__main__.E'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

d = D()
e = E()

print(d.method())  # B (B comes before C in D's MRO)
print(e.method())  # C (C comes before B in E's MRO)

Method Overriding

Child classes can override parent methods to provide specific implementations:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def get_salary(self):
        return self.salary

    def get_bonus(self):
        return self.salary * 0.1

    def get_total_compensation(self):
        return self.get_salary() + self.get_bonus()

class Manager(Employee):
    def __init__(self, name, salary, department):
        super().__init__(name, salary)
        self.department = department

    def get_bonus(self):  # Override parent method
        return self.salary * 0.2  # Managers get higher bonus

    def manage_team(self):
        return f"{self.name} is managing the {self.department} team"

class Developer(Employee):
    def __init__(self, name, salary, programming_language):
        super().__init__(name, salary)
        self.programming_language = programming_language

    def get_bonus(self):  # Override parent method
        return self.salary * 0.15  # Developers get medium bonus

    def code(self):
        return f"{self.name} is coding in {self.programming_language}"

# Usage
manager = Manager("Alice", 80000, "Engineering")
developer = Developer("Bob", 70000, "Python")

print(f"{manager.name}: ${manager.get_total_compensation()}")      # Alice: $96000
print(f"{developer.name}: ${developer.get_total_compensation()}")  # Bob: $80500

Using super()

The super() function is used to call methods from the parent class:

class Parent:
    def __init__(self, name):
        self.name = name
        print(f"Parent constructor called for {name}")

    def method(self):
        return f"Parent method from {self.name}"

class Child(Parent):
    def __init__(self, name, age):
        super().__init__(name)  # Call parent constructor
        self.age = age
        print(f"Child constructor called for {name}")

    def method(self):
        parent_result = super().method()  # Call parent method
        return f"Child method: {parent_result}, Age: {self.age}"

child = Child("Charlie", 25)
# Output:
# Parent constructor called for Charlie
# Child constructor called for Charlie

print(child.method())  # Child method: Parent method from Charlie, Age: 25

Access Control in Inheritance

class Parent:
    def __init__(self):
        self.public_var = "public"
        self._protected_var = "protected"
        self.__private_var = "private"

    def public_method(self):
        return "public method"

    def _protected_method(self):
        return "protected method"

    def __private_method(self):
        return "private method"

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.child_var = "child specific"

    def access_parent_members(self):
        print(self.public_var)        # Accessible
        print(self._protected_var)    # Accessible (convention)
        # print(self.__private_var)   # Not accessible (name mangling)

        print(self.public_method())   # Accessible
        print(self._protected_method())  # Accessible
        # print(self.__private_method())  # Not accessible

child = Child()
child.access_parent_members()

Abstract Base Classes and Inheritance

from abc import ABC, abstractmethod

class Shape(ABC):
    def __init__(self, color):
        self.color = color

    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

    def get_color(self):
        return self.color

class Circle(Shape):
    def __init__(self, color, radius):
        super().__init__(color)
        self.radius = radius

    def area(self):  # Must implement abstract method
        import math
        return math.pi * self.radius ** 2

    def perimeter(self):  # Must implement abstract method
        import math
        return 2 * math.pi * self.radius

class Rectangle(Shape):
    def __init__(self, color, width, height):
        super().__init__(color)
        self.width = width
        self.height = height

    def area(self):  # Must implement abstract method
        return self.width * self.height

    def perimeter(self):  # Must implement abstract method
        return 2 * (self.width + self.height)

# Cannot instantiate abstract class
# shape = Shape("Red")  # TypeError

# Can instantiate concrete subclasses
circle = Circle("Red", 5)
rectangle = Rectangle("Blue", 4, 6)

print(f"Circle area: {circle.area():.2f}")
print(f"Rectangle perimeter: {rectangle.perimeter()}")

Method Chaining with Inheritance

class Animal:
    def __init__(self, name):
        self.name = name
        self.energy = 100

    def eat(self):
        self.energy += 20
        print(f"{self.name} ate and gained energy")
        return self  # Return self for chaining

    def sleep(self):
        self.energy += 30
        print(f"{self.name} slept and gained energy")
        return self

    def get_energy(self):
        return self.energy

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def play(self):
        self.energy -= 15
        print(f"{self.name} played and lost energy")
        return self

    def bark(self):
        print(f"{self.name} barks!")
        return self

# Method chaining
dog = Dog("Buddy", "Golden Retriever")
dog.eat().sleep().play().bark()
print(f"Energy: {dog.get_energy()}")  # Energy: 135

Inheritance vs Composition

Inheritance (Is-A Relationship)

class Vehicle:
    def start_engine(self):
        return "Engine started"

class Car(Vehicle):  # Car IS-A Vehicle
    def drive(self):
        return "Car is driving"

car = Car()
car.start_engine()  # Inherited method
car.drive()         # Own method

Composition (Has-A Relationship)

class Engine:
    def start(self):
        return "Engine started"

class Car:
    def __init__(self):
        self.engine = Engine()  # Car HAS-A Engine

    def start_engine(self):
        return self.engine.start()

    def drive(self):
        return "Car is driving"

car = Car()
car.start_engine()  # Delegated to engine
car.drive()         # Own method

Built-in Functions for Inheritance

class Animal:
    pass

class Dog(Animal):
    pass

class Cat(Animal):
    pass

dog = Dog()
cat = Cat()

# Check inheritance relationships
print(isinstance(dog, Dog))      # True
print(isinstance(dog, Animal))   # True
print(isinstance(dog, Cat))      # False

print(issubclass(Dog, Animal))   # True
print(issubclass(Cat, Animal))   # True
print(issubclass(Dog, Cat))      # False

# Get class hierarchy
print(Dog.__bases__)             # (<class '__main__.Animal'>,)
print(Animal.__bases__)          # (<class 'object'>,)

# Get MRO
print(Dog.__mro__)               # (<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>)

Summary Table

Type Description Example
Single Inheritance One parent class class Child(Parent):
Multiple Inheritance Multiple parent classes class Child(Parent1, Parent2):
Multilevel Inheritance Chain of inheritance A → B → C
Hierarchical Inheritance Multiple children from one parent Parent → Child1, Child2
Method Overriding Redefine parent method def method(self): return "new"
super() Call parent methods super().__init__(name)
MRO Method resolution order Class.__mro__
Abstract Classes Force method implementation @abstractmethod

Key Interview Points

  1. Inheritance promotes code reuse and establishes “is-a” relationships
  2. Single inheritance is most common and straightforward
  3. Multiple inheritance can lead to complexity (diamond problem)
  4. Method Resolution Order (MRO) determines method lookup order
  5. Method overriding allows child classes to provide specific implementations
  6. super() is used to call parent class methods
  7. Abstract Base Classes enforce method implementation in subclasses
  8. Inheritance vs Composition - choose based on relationship type
  9. Access control affects what child classes can access from parents
  10. Python supports all major inheritance types

Benefits of Inheritance

  • Code Reuse: Avoid duplicating common functionality
  • Hierarchy: Model real-world relationships naturally
  • Polymorphism: Use child classes where parent classes are expected
  • Maintainability: Changes in parent affect all children
  • Extensibility: Easy to add new functionality
  • Consistency: Common interface across related classes

Inheritance is a powerful tool for creating organized, reusable, and maintainable object-oriented code!

Interview angle

  • “How does Python resolve a method with multiple inheritance?” — the MRO, computed by C3 linearisation, which preserves each parent’s order and guarantees a class appears before its own parents. Inspect it with Cls.__mro__.
  • “What does super() actually do?” — it follows the MRO from the current class, not “the parent class”. In a diamond, super() in the middle class can dispatch to a sibling rather than the base — which is the whole point, and why cooperative multiple inheritance needs every class to call super().
  • “Why does super().__init__() matter in multiple inheritance?” — if one class in the chain doesn’t call it, the rest of the MRO never runs and those parents are silently never initialised.
  • “Inheritance or composition?” — composition by default. Inheritance is for genuine substitutability (Liskov): a subclass must be usable everywhere the base is. Reaching for inheritance to reuse a method couples you to the whole base class. See ../13_architecture_design/05_composition_over_inheritance.md.