Ad – 728Γ—90
πŸ—οΈ OOP

Python Inheritance – Single, Multiple, and Multilevel

Inheritance is one of the four pillars of object-oriented programming. It lets a class inherit attributes and methods from another class, promoting code reuse and establishing clear hierarchies of related types. Python supports single, multiple, and multilevel inheritance β€” along with powerful tools like super(), the Method Resolution Order (MRO), abstract base classes, and built-in type-checking functions. This lesson covers all of them with real-world examples.

⏱️ 28 min read 🎯 Intermediate / OOP πŸ“… Updated 2026

What Is Inheritance?

Inheritance is the mechanism by which one class (the child or subclass) acquires the properties and behaviour of another class (the parent or superclass). Instead of repeating code, you write it once in the parent and let children extend or specialise it.

Real-world analogy: a Dog and a Cat are both Animals. They share common behaviour (eat, sleep, breathe) defined in Animal, but each has its own specific behaviour (bark vs meow).

Python
class Animal:
    """Parent / Base class."""
    def __init__(self, name, species):
        self.name = name
        self.species = species

    def breathe(self):
        return f"{self.name} breathes air."

    def eat(self, food):
        return f"{self.name} eats {food}."

    def __repr__(self):
        return f"{self.__class__.__name__}(name={self.name!r})"


# ─── Single Inheritance ───────────────────────────────────────
class Dog(Animal):
    """Child class inheriting from Animal."""
    def bark(self):
        return f"{self.name} says: Woof!"

class Cat(Animal):
    """Another child class."""
    def meow(self):
        return f"{self.name} says: Meow!"

dog = Dog("Rex", "Canis lupus familiaris")
cat = Cat("Whiskers", "Felis catus")

# Inherited methods work on child instances
print(dog.breathe())        # Rex breathes air.
print(dog.eat("kibble"))    # Rex eats kibble.
print(dog.bark())           # Rex says: Woof!
print(cat.meow())           # Whiskers says: Meow!
β–Ά Output
Rex breathes air. Rex eats kibble. Rex says: Woof! Whiskers says: Meow!

Using super() – Calling the Parent

super() returns a proxy object that lets you call methods from the parent class. It is essential in __init__ to initialise the parent's attributes before adding child-specific ones:

Python
class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species
        print(f"Animal.__init__: created {name}")

class Dog(Animal):
    def __init__(self, name, breed):
        # Call parent's __init__ first
        super().__init__(name, "Canis lupus familiaris")
        self.breed = breed
        print(f"Dog.__init__: breed set to {breed}")

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

rex = Dog("Rex", "German Shepherd")
print(rex.info())
β–Ά Output
Animal.__init__: created Rex Dog.__init__: breed set to German Shepherd Rex is a German Shepherd (Canis lupus familiaris)
πŸ’‘
Always Call super().__init__() in Child Classes

Forgetting to call super().__init__() means the parent's initialisation code never runs. This leads to missing attributes and mysterious AttributeErrors. Make it a habit.

Method Overriding

A child class can override a parent method by defining a method with the same name. The child version replaces the parent's version for instances of the child class:

Python
class Shape:
    def area(self):
        raise NotImplementedError("Subclasses must implement area()")

    def describe(self):
        return f"I am a {self.__class__.__name__} with area {self.area():.2f}"

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

    def area(self):                      # Overrides Shape.area()
        return 3.14159 * self.radius ** 2

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

    def area(self):                      # Overrides Shape.area()
        return self.width * self.height

class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)     # Reuse Rectangle.__init__

# Polymorphism: same method name, different behaviour
shapes = [Circle(5), Rectangle(4, 6), Square(3)]
for shape in shapes:
    print(shape.describe())
β–Ά Output
I am a Circle with area 78.54 I am a Rectangle with area 24.00 I am a Square with area 9.00
Ad – 336Γ—280

Multilevel Inheritance

A class can inherit from a class that itself inherits from another, creating a chain. Each level can extend or override the behaviour above it:

Python
class Vehicle:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

    def start(self):
        return f"{self.make} {self.model} starts."

class Car(Vehicle):
    def __init__(self, make, model, year, doors):
        super().__init__(make, model, year)
        self.doors = doors

    def honk(self):
        return "Beep beep!"

class ElectricCar(Car):
    def __init__(self, make, model, year, doors, battery_kwh):
        super().__init__(make, model, year, doors)
        self.battery_kwh = battery_kwh

    def charge_status(self):
        return f"Battery: {self.battery_kwh} kWh"

    def start(self):   # Override Vehicle.start()
        return f"{self.make} {self.model} starts silently."

tesla = ElectricCar("Tesla", "Model 3", 2024, 4, 75)
print(tesla.start())          # ElectricCar override
print(tesla.honk())           # From Car
print(tesla.charge_status())  # From ElectricCar
print(f"Year: {tesla.year}")  # From Vehicle
β–Ά Output
Tesla Model 3 starts silently. Beep beep! Battery: 75 kWh Year: 2024

Multiple Inheritance

Python allows a class to inherit from more than one parent. This is powerful but must be used carefully to avoid ambiguity:

Python
class Flyable:
    def fly(self):
        return f"{self.__class__.__name__} is flying!"

    def describe(self):
        return "I can fly."

class Swimmable:
    def swim(self):
        return f"{self.__class__.__name__} is swimming!"

    def describe(self):
        return "I can swim."

class Duck(Flyable, Swimmable):
    """Duck can both fly and swim."""
    def quack(self):
        return "Quack!"

    # Python uses MRO to resolve describe() – Flyable wins (listed first)
    # We can be explicit:
    def describe(self):
        return "I can fly AND swim."

donald = Duck()
print(donald.fly())       # Duck is flying!
print(donald.swim())      # Duck is swimming!
print(donald.quack())     # Quack!
print(donald.describe())  # I can fly AND swim.

# Mixins – the most common use of multiple inheritance
class JSONMixin:
    """Add JSON serialisation to any class."""
    import json as _json
    def to_json(self):
        import json
        return json.dumps(self.__dict__, default=str)

class LogMixin:
    """Add simple logging to any class."""
    def log(self, msg):
        print(f"[{self.__class__.__name__}] {msg}")

class UserService(LogMixin, JSONMixin):
    def __init__(self, user_id, email):
        self.user_id = user_id
        self.email = email

svc = UserService(42, "alice@example.com")
svc.log("User service initialised")
print(svc.to_json())
β–Ά Output
[UserService] User service initialised {"user_id": 42, "email": "alice@example.com"}

Method Resolution Order (MRO)

When Python searches for a method in a class hierarchy, it follows a deterministic algorithm called the C3 linearisation. You can inspect the MRO via ClassName.__mro__ or help(ClassName):

Python
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

# MRO: D β†’ B β†’ C β†’ A β†’ object
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
#  <class '__main__.A'>, <class 'object'>)

d = D()
print(d.method())   # "B"  – first in MRO after D that defines method()

# mro() method returns a list
for cls in D.mro():
    print(cls.__name__)
β–Ά Output
D B C A object
ℹ️
The Diamond Problem

The classic problem with multiple inheritance: if D inherits from B and C which both inherit from A, which A.__init__ gets called? Python's C3 MRO and cooperative super() resolve this cleanly β€” each class in the MRO is called exactly once when all classes use super().

isinstance() and issubclass()

These two built-in functions let you check type relationships at runtime:

Python
class Animal: pass
class Dog(Animal): pass
class Cat(Animal): pass

rex = Dog()
whiskers = Cat()

# isinstance checks if an object is an instance of a class (or its subclasses)
print(isinstance(rex, Dog))     # True
print(isinstance(rex, Animal))  # True  ← Dog IS-A Animal
print(isinstance(rex, Cat))     # False

# Check against multiple types using a tuple
print(isinstance(rex, (Dog, Cat)))   # True – matches Dog

# issubclass checks class relationships, not instances
print(issubclass(Dog, Animal))  # True
print(issubclass(Cat, Dog))     # False
print(issubclass(Dog, object))  # True – everything inherits from object

# Practical use: type-safe function
def make_sound(animal):
    if not isinstance(animal, Animal):
        raise TypeError(f"Expected Animal, got {type(animal).__name__}")
    if isinstance(animal, Dog):
        return "Woof!"
    if isinstance(animal, Cat):
        return "Meow!"
    return "..."

print(make_sound(rex))       # Woof!
print(make_sound(whiskers))  # Meow!

Abstract Base Classes

Abstract classes define a contract: subclasses must implement certain methods. Python enforces this with the abc module:

Python
from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    """Abstract base class β€” cannot be instantiated directly."""

    @abstractmethod
    def charge(self, amount: float) -> bool:
        """Charge the customer. Return True on success."""
        ...

    @abstractmethod
    def refund(self, amount: float) -> bool:
        """Refund the customer. Return True on success."""
        ...

    # Concrete method shared by all gateways
    def process(self, amount: float) -> str:
        if amount <= 0:
            raise ValueError("Amount must be positive")
        success = self.charge(amount)
        return f"Payment of ${amount:.2f} {'succeeded' if success else 'failed'}."

class StripeGateway(PaymentGateway):
    def charge(self, amount: float) -> bool:
        print(f"[Stripe] Charging ${amount:.2f}...")
        return True

    def refund(self, amount: float) -> bool:
        print(f"[Stripe] Refunding ${amount:.2f}...")
        return True

class PayPalGateway(PaymentGateway):
    def charge(self, amount: float) -> bool:
        print(f"[PayPal] Charging ${amount:.2f}...")
        return True

    def refund(self, amount: float) -> bool:
        print(f"[PayPal] Refunding ${amount:.2f}...")
        return True

# Cannot instantiate abstract class:
# gateway = PaymentGateway()  β†’ TypeError!

stripe = StripeGateway()
print(stripe.process(49.99))

paypal = PayPalGateway()
print(paypal.process(19.99))
β–Ά Output
[Stripe] Charging $49.99... Payment of $49.99 succeeded. [PayPal] Charging $19.99... Payment of $19.99 succeeded.
Inheritance TypeSyntaxUse case
Singleclass B(A)Standard extension / specialisation
Multilevelclass C(B) where B extends ADeep taxonomies, layered frameworks
Multipleclass D(A, B)Mixins, combining orthogonal capabilities
Abstractclass E(ABC) with @abstractmethodDefining interfaces / contracts

πŸ‹οΈ Practical Exercise

Design a small OOP hierarchy for a library system:

  1. Create an abstract class LibraryItem with abstract method get_info(), and concrete attributes title, item_id.
  2. Create Book(LibraryItem) that adds author and pages.
  3. Create DVD(LibraryItem) that adds director and runtime_minutes.
  4. Create EBook(Book) that adds file_size_mb and overrides get_info() to note it is digital.
  5. Write a function that accepts a list of LibraryItem objects and prints info for each.

πŸ”₯ Challenge Exercise

Implement a plugin system using abstract base classes. Create an abstract DataSource with methods connect(), fetch(query), and disconnect(). Implement two concrete classes: CSVDataSource and JSONDataSource. Write a DataPipeline class that accepts any DataSource, calls its methods, and is completely agnostic of the concrete implementation. Use isinstance to validate input types.

Interview Questions

  • What is inheritance and what problem does it solve?
  • What is the difference between a base class and a derived class?
  • What does super() do? Why is it preferred over calling the parent class directly?
  • What is method overriding and how does Python resolve it?
  • What is the Method Resolution Order (MRO) in Python?
  • What is the diamond problem in multiple inheritance and how does Python solve it?
  • What is the difference between isinstance() and type()?
  • What is an abstract class and when would you use one?
  • What is the difference between @abstractmethod and a regular method that raises NotImplementedError?
  • What does issubclass(X, Y) return when X and Y are the same class?

πŸ“‹ Summary

  • Inheritance allows a child class to reuse attributes and methods from a parent class.
  • Single inheritance: one parent. Multilevel: chain of parents. Multiple: more than one parent.
  • Always call super().__init__() in a child's __init__ to ensure the parent is properly initialised.
  • Method overriding lets a child class replace a parent method; super().method() calls the parent version.
  • The MRO (C3 linearisation) determines which class's method is found first; inspect it with ClassName.__mro__.
  • isinstance(obj, Class) returns True if obj is an instance of Class or any subclass.
  • issubclass(Child, Parent) returns True if Child inherits from Parent.
  • Abstract base classes (via abc.ABC and @abstractmethod) enforce that subclasses implement required methods.

Frequently Asked Questions

Does Python support multiple inheritance? Is it safe to use? +

Yes, Python fully supports multiple inheritance. It is safe when used with care β€” particularly the Mixin pattern, where classes add narrow, orthogonal capabilities (e.g., LogMixin, JSONMixin) without sharing state. Avoid deep multiple-inheritance hierarchies where classes share attributes and methods, as this creates ambiguity and maintainability issues.

When should I use composition over inheritance? +

The rule of thumb is: use inheritance when the relationship is "IS-A" (a Dog IS-A Animal), and use composition when it is "HAS-A" (a Car HAS-A Engine). Composition is often more flexible because you can swap out the composed object at runtime, whereas changing a parent class affects the entire hierarchy.

What does every Python class inherit from if no parent is specified? +

Every class implicitly inherits from object in Python 3. This gives all classes access to methods like __init__, __str__, __repr__, __eq__, and __hash__. You can verify this with MyClass.__mro__ β€” object always appears at the end.

What is the difference between an interface (abstract class) and a mixin? +

An abstract base class (interface) defines a contract: it declares what methods must exist but provides no implementation. A mixin provides a concrete implementation of one specific capability that is designed to be mixed into many unrelated classes. In practice, Python uses abstract base classes for type checking and mixins for code reuse through multiple inheritance.