Inheritance in Python: Types, MRO, and Working Examples
Python supports 5 inheritance types. This guide covers single, multiple, multilevel, hierarchical, and hybrid inheritance with MRO, super(), and placement-round patterns.
Inheritance lets a Python class reuse attributes and methods from another class without rewriting them.
What Inheritance Does in Python
When you write class Engineer(Employee):, Python gives Engineer access to every attribute and method defined in Employee. The child class can override any method, add new ones, or use the parent’s implementation as-is. This single mechanism drives most of the code reuse in object-oriented Python.
Three things happen at class definition time:
- Python stores the parent reference in the child’s
__bases__tuple. - Every method and attribute from the parent becomes accessible on child instances via the standard lookup chain.
- If the child defines a method with the same name, the child’s version takes priority (method overriding).
The syntax is minimal:
class Employee:
def __init__(self, name, emp_id):
self.name = name
self.emp_id = emp_id
def summary(self):
return f"{self.name} (ID: {self.emp_id})"
class Engineer(Employee):
def __init__(self, name, emp_id, domain):
super().__init__(name, emp_id)
self.domain = domain
def summary(self):
return f"{super().summary()} — {self.domain}"
dev = Engineer("Arun", "E1042", "backend")
print(dev.summary())
# Output: Arun (ID: E1042) — backend
Engineer inherits Employee’s __init__ logic via super().__init__(), then adds its own domain attribute. The summary() method overrides the parent version while still referencing it through super(). This pattern of extending rather than replacing parent behaviour is idiomatic Python. You keep the parent’s logic intact and layer additional behaviour on top.
The Five Inheritance Types
Python supports five inheritance patterns. Each describes a different shape of class relationship.
Single Inheritance
One child, one parent. The simplest form.
class Vehicle:
def start(self):
return "Engine started"
class Car(Vehicle):
def drive(self):
return "Car moving"
c = Car()
print(c.start()) # Engine started
print(c.drive()) # Car moving
Multiple Inheritance
One child inherits from two or more parents.
class GPSMixin:
def locate(self):
return "GPS coordinates acquired"
class Vehicle:
def start(self):
return "Engine started"
class DeliveryTruck(Vehicle, GPSMixin):
def deliver(self):
return "Package delivered"
truck = DeliveryTruck()
print(truck.start()) # Engine started
print(truck.locate()) # GPS coordinates acquired
DeliveryTruck combines behaviour from both Vehicle and GPSMixin. The order in the parentheses matters for MRO (covered in the next section).
Multilevel Inheritance
A chain: grandparent to parent to child.
class Vehicle:
def start(self):
return "Engine started"
class Car(Vehicle):
def drive(self):
return "Car moving at 60 km/h"
class ElectricCar(Car):
def charge(self):
return "Battery charging"
ec = ElectricCar()
print(ec.start()) # Engine started (from Vehicle)
print(ec.drive()) # Car moving at 60 km/h (from Car)
print(ec.charge()) # Battery charging (own method)
ElectricCar reaches Vehicle.start() through Car, not directly.
Hierarchical Inheritance
Multiple children share one parent.
class Employee:
def __init__(self, name):
self.name = name
def role(self):
return "General employee"
class Developer(Employee):
def role(self):
return f"{self.name}: Developer"
class Analyst(Employee):
def role(self):
return f"{self.name}: Analyst"
d = Developer("Priya")
a = Analyst("Karthik")
print(d.role()) # Priya: Developer
print(a.role()) # Karthik: Analyst
Both Developer and Analyst inherit from Employee but override role() differently.
Hybrid Inheritance
A combination of multiple and multilevel (or hierarchical) inheritance. The classic case forms a diamond shape.
class Vehicle:
def __init__(self):
self.fuel_type = "generic"
print("Vehicle.__init__")
class ElectricVehicle(Vehicle):
def __init__(self):
super().__init__()
self.fuel_type = "electric"
print("ElectricVehicle.__init__")
class PassengerVehicle(Vehicle):
def __init__(self):
super().__init__()
self.seats = 5
print("PassengerVehicle.__init__")
class ElectricCar(ElectricVehicle, PassengerVehicle):
def __init__(self):
super().__init__()
print("ElectricCar.__init__")
ec = ElectricCar()
Output:
- Vehicle.
__init__ - PassengerVehicle.
__init__ - ElectricVehicle.
__init__ - ElectricCar.
__init__
Vehicle.__init__ runs only once despite appearing in both parent chains. That is MRO at work.
| Type | Structure | Use when |
|---|---|---|
| Single | A → B | Extending one base class |
| Multiple | A, B → C | Combining orthogonal behaviours (mixins) |
| Multilevel | A → B → C | Building specialisation chains |
| Hierarchical | A → B, A → C | Shared base with diverging specialisations |
| Hybrid | Diamond or complex graph | Real-world models with overlapping hierarchies |
Method Resolution Order and the Diamond Problem
When ElectricCar calls a method, Python needs a rule for which parent to check first. That rule is MRO, computed via C3 linearisation.
Inspect it directly:
print(ElectricCar.__mro__)
Output:
(<class 'ElectricCar'>, <class 'ElectricVehicle'>,
<class 'PassengerVehicle'>, <class 'Vehicle'>, <class 'object'>)
The algorithm guarantees three properties:
- A child always appears before its parents.
- The left-to-right order in the class definition is preserved.
- If a consistent ordering is impossible (conflicting parent orders), Python raises
TypeErrorat class definition time.
ElectricVehicle comes before PassengerVehicle because it was listed first in class ElectricCar(ElectricVehicle, PassengerVehicle). If you swap the order in the definition, MRO flips accordingly.
The super() Bug with Direct Parent Calls
A common mistake in multiple inheritance is calling the parent __init__ directly instead of using super():
# BUG: skips MRO, calls Vehicle.__init__ twice
class ElectricVehicle(Vehicle):
def __init__(self):
Vehicle.__init__(self) # direct call
self.fuel_type = "electric"
class PassengerVehicle(Vehicle):
def __init__(self):
Vehicle.__init__(self) # direct call
self.seats = 5
class ElectricCar(ElectricVehicle, PassengerVehicle):
def __init__(self):
ElectricVehicle.__init__(self)
PassengerVehicle.__init__(self)
Here Vehicle.__init__ runs twice. In a real codebase with database connections or file handles opened in __init__, this causes duplicate resource allocation.
The fix: replace every Parent.__init__(self) with super().__init__(). Then MRO ensures each class in the chain is initialised exactly once.
Edge Cases and Common Mistakes
__bases__ vs __mro__
print(ElectricCar.__bases__)
# (<class 'ElectricVehicle'>, <class 'PassengerVehicle'>)
print(ElectricCar.__mro__)
# (<class 'ElectricCar'>, <class 'ElectricVehicle'>,
# <class 'PassengerVehicle'>, <class 'Vehicle'>, <class 'object'>)
__bases__ shows only the immediate parents. __mro__ shows the complete lookup chain. Interview output-prediction questions often test this distinction.
isinstance() vs type()
ec = ElectricCar()
print(isinstance(ec, Vehicle)) # True
print(type(ec) == Vehicle) # False
print(type(ec) == ElectricCar) # True
isinstance() walks the inheritance hierarchy. type() checks the exact class. Use isinstance() when you care about substitutability (“is this object usable as a Vehicle?”); use type() when you need an exact-class match. Most production code and placement-round answers expect isinstance() for type-checking.
Abstract Base Classes and Inheritance
The abc module enforces method contracts across inheritance chains:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
# Shape() would raise TypeError
r = Rectangle(4, 5)
print(r.area()) # 20
If Rectangle forgot to define area(), Python raises TypeError at instantiation, not at the method call. This enforcement happens even if area() is never called at runtime. The abstract contract is checked structurally, not dynamically. This is how large codebases enforce interface compliance in inherited hierarchies without relying on integration tests to catch missing methods.
Where Inheritance Shows Up in Placement Rounds
OOP theory is a recurring section in written rounds for service-tier companies. The patterns:
- Output-prediction on MRO (which
__init__prints first) - MCQs on
super()behaviour in multilevel chains - Code-correction questions where
Parent.__init__(self)causes a bug
These questions test whether you can trace execution order, not whether you memorised definitions. If you can read ClassName.__mro__ and predict output, you clear that section reliably. The typical format is a 4-class diamond hierarchy printed to stdout, with the question asking you to sequence the print statements.
For algorithmic rounds that follow the OOP section, practise problems like finding array extremes or palindrome checks where clean class design keeps your solution readable. Common data structure interview questions also assume you can wrap solutions in well-structured classes.
The diamond-problem example from this article (ElectricCar inheriting from both ElectricVehicle and PassengerVehicle) is a pattern you can build, break, and debug interactively.
- TinkerLLM gives you a prompt-driven Python sandbox for exactly that kind of MRO experiment at ₹299.
Primary sources
Frequently asked questions
What is the difference between __bases__ and __mro__ in Python?
__bases__ returns only the immediate parent classes listed in the class definition. __mro__ returns the entire method resolution chain including all ancestors up to object, computed by C3 linearisation.
Can a child class inherit from more than two parents in Python?
Yes. Python places no hard limit on the number of parents. The class definition lists them separated by commas: class Child(A, B, C, D). MRO still resolves method lookup in a deterministic left-to-right, depth-first order via C3 linearisation.
What happens if two parent classes define the same method name?
Python uses MRO to decide. It calls the method from whichever parent appears first in the resolution order. You can inspect the order by printing ChildClass.__mro__ and checking which parent comes earlier in the tuple.
Does Python support private inheritance like C++ does?
No. Python has no access-specifier-based inheritance modes (public, protected, private) like C++. All inherited attributes and methods are accessible from the child class. Name mangling with a __ prefix is the closest equivalent, but it is not true access restriction.
How does super() know which parent to call in multiple inheritance?
super() follows the MRO. It does not simply call the immediate parent in the source code. It calls the next class in the __mro__ tuple relative to the class where super() is invoked. This ensures every class in the chain is called exactly once.
A self-paced playground for building with LLMs.
TinkerLLM is FACE Prep's sister property. A guided environment for shipping real LLM applications, the kind of project that earns a paragraph on your resume, not a line.
Try TinkerLLM (₹299 launch)