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
- Inheritance promotes code reuse and establishes “is-a” relationships
- Single inheritance is most common and straightforward
- Multiple inheritance can lead to complexity (diamond problem)
- Method Resolution Order (MRO) determines method lookup order
- Method overriding allows child classes to provide specific implementations
super()is used to call parent class methods- Abstract Base Classes enforce method implementation in subclasses
- Inheritance vs Composition - choose based on relationship type
- Access control affects what child classes can access from parents
- 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 callsuper(). - “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.