Inheritance in Python: Types, Method Resolution Order, super(), and Best Practices
Learn Python inheritance with practical examples covering parent-child classes, super(), method overriding, MRO, multiple and hierarchical inheritance, and object-oriented programming (OOP) best practices for writing maintainable, scalable applications.
If Encapsulation is about protecting data and abstraction is about hiding complexity, inheritance is about reusing and extending existing code. It’s the third pillar of Object-Oriented Programming, and arguably the one that saves developers the most repetitive work.
What Is Inheritance?
Inheritance is a mechanism where one class acquires the attributes and methods of another class. The existing class is called the Parent (Base/Super) class, and the new class is called the Child (Derived/Sub) class. Instead of writing everything again, the child automatically gets everything the parent already has.
Think of a Vehicle — it has an engine, wheels, and a start() method. A Car doesn’t need to redefine any of that. It simply inherits Vehicle, then adds its own extras: a sunroof, a music system, a reverse camera. The car gets everything the vehicle already had, plus whatever it adds on top.
Vehicle
↑
Car
Why Do We Need It?
Without inheritance, Car, Bike, and Truck would each need their own start(), stop(), and engine() methods — full of duplicate code. With inheritance, one Vehicle class defines all three behaviors once, and Car, Bike, and Truck simply inherit them. One implementation. Reusable. Maintainable.
This reuse brings several concrete benefits: less duplicated code, easier maintenance, natural hierarchical relationships, better scalability, support for polymorphism, and far easier future extensions.
Basic Syntax and a First Example
class Parent:
...
class Child(Parent):
...
Child automatically gets everything from Parent. Here’s the simplest possible demonstration:
class Animal:
def eat(self):
print("Animal is eating")
class Dog(Animal):
pass
dog = Dog()
dog.eat()
Output: Animal is eating
Dog never defined eat() — it inherited it. When Python looks for eat(), it checks the Dog class first, doesn’t find it, then walks up to Animal and finds it there.
Parent and Child in Practice
A parent class typically holds common, shared functionality:
class Vehicle:
def start(self):
print("Vehicle started")
def stop(self):
print("Vehicle stopped")
A child class extends it with its own additions:
class Car(Vehicle):
def play_music(self):
print("Music playing")
car = Car()
car.start()
car.play_music()
Output:
Vehicle started
Music playing
The IS-A Relationship
Inheritance models an IS-A relationship: a Dog IS-A Animal, a Car IS-A Vehicle, a Teacher IS-A Employee, a Student IS-A Person. This is the litmus test for whether inheritance is the right tool.
Not everything fits this pattern. An Engine is not a Car, and a Wheel is not a Car — those are HAS-A relationships, better modeled with composition (an object holding another object as an attribute) rather than inheritance.
What Gets Inherited
A child class inherits variables, methods, class variables, static methods, and class methods — unless something restricts that access.
class Parent:
school = "ABC School"
def study(self):
print("Studying")
class Student(Parent):
pass
s = Student()
print(s.school)
s.study()
Output:
ABC School
Studying
Constructors and super()
The parent’s constructor is inherited by default:
class Person:
def __init__(self):
print("Person created")
class Student(Person):
pass
Student()
Output: Person created
But if the child defines its own constructor, the parent’s constructor no longer runs automatically:
class Person:
def __init__(self):
print("Person")
class Student(Person):
def __init__(self):
print("Student")
Student()
Output: Student
This happens because the child’s constructor overrides the parent’s. Python calls whichever __init__ belongs to the actual object being created — not the parent’s automatically.
To run the parent’s version anyway, use super():
class Person:
def __init__(self):
print("Person created")
class Student(Person):
def __init__(self):
super().__init__()
print("Student created")
Student()
Output:
Person created
Student created
super() is preferred over calling Parent.__init__(self) directly — it’s cleaner, supports multiple inheritance correctly, and is the accepted industry standard. The same pattern works for regular methods, not just constructors:
class Animal:
def speak(self):
print("Animal sound")
class Dog(Animal):
def bark(self):
super().speak()
print("Dog barking")
Dog().bark()
Output:
Animal sound
Dog barking
Method Overriding
Overriding lets a child class replace the parent’s implementation with its own version:
class Animal:
def sound(self):
print("Some sound")
class Dog(Animal):
def sound(self):
print("Bark")
Calling Dog().sound() prints Bark. This matters because different children genuinely behave differently — a Vehicle.move() might mean drive for a Car, fly for a Plane, and sail for a Boat. Same method name, different behavior per class.
You can also override a method and still call the parent’s version inside it:
class Animal:
def sound(self):
print("Animal sound")
class Dog(Animal):
def sound(self):
super().sound()
print("Dog Bark")
Output:
Animal sound
Dog Bark
Types of Inheritance
Python supports five patterns of inheritance:
- Single — one parent, one child (A → B)
- Multilevel — a chain (A → B → C)
- Multiple — one child, two or more parents (A + B → C)
- Hierarchical — one parent, several children (A → B, C, D)
- Hybrid — a combination of the above patterns
Single inheritance:
class Animal:
def eat(self):
print("Eating")
class Dog(Animal):
pass
Multilevel inheritance:
class Grandparent:
def house(self):
print("House")
class Parent(Grandparent):
def car(self):
print("Car")
class Child(Parent):
pass
child = Child()
child.house()
child.car()
Hierarchical inheritance — both Dog and Cat inherit from the same Animal:
class Animal:
def eat(self):
print("Eating")
class Dog(Animal):
pass
class Cat(Animal):
pass
Multiple inheritance — a child pulling from two unrelated parents:
class Father:
def bike(self):
print("Bike")
class Mother:
def jewelry(self):
print("Jewelry")
class Child(Father, Mother):
pass
child = Child()
child.bike()
child.jewelry()
Hybrid inheritance simply combines multiple of these patterns in one design — Python supports it natively, but it’s the pattern most likely to get complicated fast.
Method Resolution Order (MRO)
When multiple parent classes define the same method, which one wins?
class A:
def show(self):
print("A")
class B:
def show(self):
print("B")
class C(A, B):
pass
C().show()
Output: A
Python resolves this using the Method Resolution Order — it searches C, then A, then B, then object, and calls the first match it finds. You can inspect this order directly:
print(C.mro())
Output: [C, A, B, object]
Every class in Python, including custom ones with no explicit parent, ultimately inherits from the built-in object class.
Type Checks: isinstance() and issubclass()
Two built-in functions help you verify relationships at runtime:
class Animal:
pass
class Dog(Animal):
pass
dog = Dog()
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True
print(issubclass(Dog, Animal)) # True
isinstance() checks whether an object belongs to a class (or any of its ancestors), while issubclass() checks the relationship between two classes directly.
Protected and Private Members in Inheritance
A single underscore signals a protected member — intended for use by subclasses, but not enforced by Python:
class Parent:
def __init__(self):
self._age = 30
class Child(Parent):
def show(self):
print(self._age)
A double underscore signals a private member. It can’t be accessed directly from a child class using its original name, because Python applies name mangling — internally renaming __salary to _Parent__salary to reduce accidental access:
class Parent:
def __init__(self):
self.__salary = 50000
class Child(Parent):
def show(self):
print(self._Parent__salary)
This works, but it’s a workaround, not a recommended pattern — private attributes are meant to stay encapsulated within the class that defines them; use getter methods or properties instead of reaching for the mangled name.
Composition vs Inheritance
Inheritance models IS-A: a Car IS-A Vehicle. Composition models HAS-A: a Car HAS-A Engine. The rule of thumb is simple — use inheritance for genuine specialization, and use composition for assembling objects out of parts that don’t share an IS-A relationship.
Common Mistakes
A few pitfalls show up constantly in real codebases:
- Forgetting
super()— the parent constructor silently never runs. - Copying code instead of inheriting — defeats the entire purpose of reuse.
- Modeling the wrong relationship — making
EngineinheritCarwhen it should be composition. - Accessing private variables directly — reaching for
self.__salaryinstead of using proper accessors. - Deep inheritance chains — A → B → C → D → E → F looks elegant on paper but becomes very hard to maintain. Shallow hierarchies age much better.
Industry Best Practices
- Model only genuine IS-A relationships with inheritance.
- Keep parent classes generic and reusable — place only truly shared behavior there.
- Call
super()when overriding constructors or cooperative methods. - Override methods only when the child’s behavior genuinely differs.
- Favor composition over inheritance when there’s no natural IS-A relationship.
- Avoid very deep inheritance chains — they increase coupling and complexity.
- Use protected members (
_attribute) as subclass extension points, and keep private members (__attribute) properly encapsulated. - Follow the Liskov Substitution Principle: any child object should be usable wherever its parent is expected, without breaking behavior.
A Complete Real-World Example
Here’s inheritance applied to something closer to production code — an Employee base class extended by two specialized roles:
class Employee:
company = "Tech Solutions"
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
def work(self):
print(f"{self.name} is working.")
def details(self):
print(f"ID: {self.employee_id}")
print(f"Name: {self.name}")
print(f"Company: {Employee.company}")
class SoftwareEngineer(Employee):
def __init__(self, name, employee_id, language):
super().__init__(name, employee_id)
self.language = language
def work(self):
print(f"{self.name} is developing software using {self.language}.")
def code_review(self):
print(f"{self.name} is reviewing code.")
def details(self):
super().details()
print(f"Primary Language: {self.language}")
class DataScientist(Employee):
def __init__(self, name, employee_id, tool):
super().__init__(name, employee_id)
self.tool = tool
def work(self):
print(f"{self.name} is building ML models using {self.tool}.")
def analyze_data(self):
print(f"{self.name} is analyzing datasets.")
def details(self):
super().details()
print(f"Tool: {self.tool}")
engineer = SoftwareEngineer("Alice", 101, "Python")
scientist = DataScientist("Bob", 102, "TensorFlow")
print("=== Software Engineer ===")
engineer.details()
engineer.work()
engineer.code_review()
print("\n=== Data Scientist ===")
scientist.details()
scientist.work()
scientist.analyze_data()
Output:
=== Software Engineer ===
ID: 101
Name: Alice
Company: Tech Solutions
Primary Language: Python
Alice is developing software using Python.
Alice is reviewing code.
=== Data Scientist ===
ID: 102
Name: Bob
Company: Tech Solutions
Tool: TensorFlow
Bob is building ML models using TensorFlow.
Bob is analyzing datasets.
Notice how both SoftwareEngineer and DataScientist reuse Employee’s constructor and details() logic through super(), while overriding work() and adding their own specialized methods. This is inheritance doing exactly what it’s meant to do: sharing common structure while letting each subclass define its own distinct behavior.
Wrapping Up
Inheritance lets you build class hierarchies where common logic lives in one place and specialized behavior lives where it belongs. Used well — shallow hierarchies, genuine IS-A relationships, consistent use of super() — it makes code dramatically more reusable and maintainable. Used carelessly — deep chains, forced relationships, skipped constructors — it becomes one of the fastest ways to make a codebase fragile. The core skill isn’t just knowing the syntax; it’s recognizing when a relationship is truly IS-A, and reaching for composition when it isn’t.