1. Introduction
Python Inheritance is one of the core features of Object-Oriented Programming (OOP) that allows two things:
Python Inheritance solves a very practical problem: code duplication and fragile systems.
In real projects, if you start writing the same logic in multiple places, then eventually it will lead to several bugs and this creates inconsistent behavior.
In this guide, we will discuss the Python Inheritance concept from basic to advanced level.
• five types of inheritance
I will keep all these details in simple words so that you can learn maximum and start using it in your code as well.
Prerequisites: Before learning Python Inheritance, you should understand Python Classes and Objects along with basic concepts like Variables in Python, Python Functions Basics and Advanced Python Functions.
2. What is Inheritance in Python? (Simple Explanation)
Inheritance is an OOP feature that allows one class to acquire the data and behavior of another class.
In simple words:
• First, you create a parent class that contains common properties and behavior (methods).
Before going deep into this topic, let’s understand the reason behind using inheritance.
1. Avoid Code Duplication
2. Improve Readability
3. Easy Code Maintenance
4. Real-World Modeling
3. Parent Class and Child Class (Core Concepts)
If you are not fully clear about how classes and objects work internally, I recommend you to read Python Classes and Objects Explained first.
Example: Parent and Child Class
class Vehicle:
def start(self):
print("Vehicle started")
class Car(Vehicle):
def drive(self):
print("Car is driving")Explanation:
In this code, first we have defined a class Vehicle, which is the parent class.
This class contains one common method called start(), which represents the common action that any vehicle can perform.
Next, we have defined another class Car, which is the child class of Vehicle.
Inheritance is defined in code by passing the parent class name inside the parentheses of the child class:
class Car(Vehicle):
Because of this, the Car class inherits the start() method from Vehicle class, so you don’t need to define it again.
In addition to this, Car class defines its own method called drive().
This method represents behavior that is specific to a car.
So, when an object of the child class Car is created, it can use:
• the start() method from the parent class
• the drive() method from its own class
In this way, you can reuse common functionality from a parent class and extend it in a child class without rewriting code.
4. Why Inheritance Matters in Real Programs
Many people think that inheritance is just about writing less code.
But in real-world Python applications, it also gives you the benefit of controlling change in business logic.
In my professional career, I have seen that requirements change frequently in production systems.
You need to update the common logic written in the parent class (as per new requirements), and it automatically updates the behavior of all related child classes.
It helps to reduce bugs and errors as well.
Inheritance helps you to maintain a clean structure as your application grows, and you don’t need to write the same logic at multiple places.
This helps to make large Python programs:
• easier to understand
• easier to modify
• easier to scale over time
There are many Python frameworks and enterprise-grade systems that use inheritance heavily, and it becomes easier to:
• manage complexity
• enforce consistency
• support long-term maintainability
5. The __init__() Method in Inheritance
The __init__() method is used to initialize data when you create an object.
In real Python programs, object state is very important.
In inheritance, you may require both parent and child class initialization.
If the parent class initializes critical attributes, then the child class must make sure that this logic still runs.
I have seen many times that developers ignore the parent class __init__() method, which leads to bugs in inheritance-based Python code in large applications.
class Employee:
def __init__(self, name):
self.name = name
class Manager(Employee):
def __init__(self, name, team_size):
super().__init__(name)
self.team_size = team_size
# Creating an object of Manager class
m = Manager("Amit", 10)
# Accessing inherited and own attributes
print(m.name)
print(m.team_size)Output:
Amit
10
Explanation:
In the above example, first we have defined a parent class Employee that initializes the name attribute.
After that, we have defined a child class Manager that adds extra data such as team_size.
Since the parent class (Employee) already contains important initialization logic, the child class (Manager) must ensure that this logic runs.
To do this, the child class Manager explicitly calls:
super().__init__(name)
Please note that super() refers to the parent class, and calling its __init__() method executes the parent’s constructor code.
It helps in two ways:
• The name attribute is properly initialized
• The object (m) is created in a consistent and safe state
When you create an object of Manager (child) class, first super().__init__(name) initializes the parent class logic, and after that the team_size attribute is set in the child class.
Finally, the object (m) contains both parent data (name) and child data (team_size), and the object is fully initialized and ready to use in the program.
6. Types of Inheritance in Python
Inheritance in Python is not only an OOP concept – it is a smart way to organize your code so that your code can grow without breaking.
From my experience, once developers understand how inheritance actually works at runtime, it becomes very easy to design clean class hierarchies.
This is the reason why Python provides different types of inheritance, and each type is suited for a specific design need.
Let’s now discuss each type in simple words:
a) Single Inheritance in Python
Single inheritance in Python means a child class inherits from one parent class.
This is the most basic and common type of inheritance in OOP.
In real Python projects, I have seen single inheritance mostly in production code, as it is simple and predictable.
Example
class A:
def show(self):
print("Method from Class A")
class B(A):
def display(self):
print("Method from Class B")
obj = B()
obj.show()
obj.display()Output:
Method from Class A
Method from Class B
Explanation:
In the above example, first we have defined a class A and class B is the child class.
Class A has a method show() and class B has a method display().
Because class B is a child class of parent class ‘Class A’, it has two methods now:
• show() – it comes from parent class ‘Class A’
• display() – it is defined under class B
So, when we create an object (obj) of child class ‘Class B’, it can access both methods.
b) Multiple Inheritance
It allows a class to inherit from more than one parent class.
It helps you to reuse functionality from multiple sources.
There are some frameworks (like Django, scikit-learn, and PyTorch) that use multiple inheritance, but rarely in everyday business logic, where composition is usually cleaner and safer.
Example:
class A:
def show_a(self):
print("Method of A")
class B:
def show_b(self):
print("Method of B")
class C(A, B):
def show_c(self):
print("Method of C")
obj = C()
obj.show_a()
obj.show_b()
obj.show_c()Explanation:
In the above example, class A contains a method show_a() and class B contains a method called show_b().
After that, we have defined another class ‘Class C’ which is inherited from both Class A and Class B.
So, it automatically gets access to methods of both parent classes A and B.
After that, when we create an object of Class C, Python allows the same object (obj) to call methods show_a(), show_b() and its own method show_c().
In this way, Python allows multiple inheritance that allows to reuse functionality from more than one class without duplicating code.
Method Resolution Order (MRO) in Python
It is the rule Python follows to decide which method to run when the same method exists in multiple parent classes.
How Python decides MRO
Python decides MRO using a rule called C3 Linearization, and it follows a clear search order (shown below):
• First, Python searches for the method in the child class
• If it is not found in child class, then Python checks the parent classes from left to right (exactly as they are written).
• After that, Python continues searching in their parent classes
• If Python is still not able to find the method in parent classes, it finally checks the built-in object class
Example
class A:
def show(self):
print("A")
class B:
def show(self):
print("B")
class C(A, B):
pass
obj = C()
obj.show()Output:
A
Explanation:
In this example, class C is inherited from Class A and Class B, and we have written pass inside Class C because Class C doesn’t have any of its own method.
When obj.show() method is called, Python follows MRO order and first it checks the method in Class C (child class), then in Class A and then in Class B.
As show() method is found in Class A first, Python executes that method and prints “A”.
Note:
In my IT experience, I have rarely used multiple inheritance in business or application code because it makes your code hard to understand and debug.
c) Multilevel Inheritance in Python
Multilevel inheritance in Python forms a chain of inheritance where one class inherits from another derived class.
Each level of inheritance adds new behavior.
During my early days of projects, I used deep multilevel inheritance, and later I realized that short inheritance chains are easier to manage.
Example
class A:
def show_a(self):
print("Class A")
class B(A):
def show_b(self):
print("Class B")
class C(B):
def show_c(self):
print("Class C")
obj = C()
obj.show_a()
obj.show_b()
obj.show_c()Output:
Class A
Class B
Class C
Explanation:
In this program, Class C indirectly inherits from Class A through Class B.
That is why it gets access to all methods from both parent classes (Class A and Class B).
When we create the object (obj) of class C (as shown above), Python first looks in Class C, then in Class B, and finally in Class A to find methods.
That is why the below mentioned three statements work without any extra code:
obj.show_a()
obj.show_b()
obj.show_c()
My advice is to keep multilevel inheritance shallow in production code (ideally no more than 2–3 levels), because it becomes hard to track the behavior and risky to change beyond that.
If you need more behavior beyond that, then switch to composition instead of adding more inheritance.
d) Hierarchical Inheritance in Python
Hierarchical inheritance in Python means multiple child classes inherit from the same parent class.
In this type of inheritance, you only write shared behavior once and reuse it across all child classes.
Generally, you write common logic in a parent class, and all child classes automatically get it, so you don’t need to repeat the code at multiple places, and also it becomes easier to maintain your code.
Example
class A:
def common(self):
print("Common Method")
class B(A):
def show_b(self):
print("Class B method")
class C(A):
def show_c(self):
print("Class C method")
obj1 = B()
obj2 = C()
obj1.common()
obj1.show_b()
obj2.common()
obj2.show_c()Output:
Common Method
Class B method
Common Method
Class C method
Explanation:
In the above program, Class A holds the common behavior, and both classes B and C reuse it (instead of writing it again).
When we create the objects of Class B and Class C, then they automatically get access to common() method from Class A.
Now, each child class has two methods:
• its own specific method
• common() method
From my production experience, I have used this pattern multiple times.
This is mostly used in those scenarios when many parts of the system work in a similar way, but each one has a small difference.
So, you keep the common code in one parent class, and if needed, you fix it once and all related classes automatically get the update.
e) Hybrid Inheritance in Python
Hybrid inheritance is a combination of two or more inheritance types.
In simple words, hybrid inheritance means mixing more than one inheritance style in the same design – for example, hierarchical inheritance combined with multiple inheritance.
In this inheritance type, methods can come from several parent classes, so it is very important to understand MRO, and that is why this structure is complex as well.

Class A:
def show_a(self):
print("Class A")
Class B (A):
def show_b(self):
print("Class B")
Class C (A):
def show_c(self):
print("Class C")
Class D (B, C):
def show_d(self):
print("Class D")
obj = D()
obj.show_a()
obj.show_b()
obj.show_c()
obj.show_d()Output: -
Class A
Class B
Class C
Class D
In this program, class A contains common functionality that is shared across the hierarchy.
Class B and C both inherit from class A, which is hierarchical inheritance, because multiple child classes come from the same parent class.
Also, class D is inherited from class B and C, which is multiple inheritance.
So, in the above program, we are using hierarchical inheritance with multiple inheritance.
So the overall structure is called hybrid inheritance.
When obj.show_a() is called, Method Resolution Order (MRO) is followed by Python and it checks classes in the following order:
D → B → C → A → Object
• D doesn’t have show_a()
• B doesn’t have show_a()
• C doesn’t have show_a()
• A does, so Python calls A.show_a()
So, Python calls the method from class A (not from B or C).
This step-by-step lookup is defined by MRO and it ensures that Python always finds the method in a clean and predictable way.
I have seen multiple times that some developers don’t understand this MRO flow.
On the other side, if you (as a developer) know that Python searches in the following order:
Child → Parents → Grandparents
then debugging becomes much easier and one can easily work with hybrid inheritance.
7. Method Overriding in Python
Method overriding in Python occurs when a child class provides its own implementation for a method that already exists in the parent class.
It is a key concept in object-oriented programming in Python.
• Method overriding in Python occurs only when inheritance is used. A child class must inherit from a parent class.

Example 1:- Method Overriding in Python
class Parent:
def my_method(self):
print("This is the parent method")
class Child(Parent):
def my_method(self):
print("This is the child method")
obj = Child()
obj.my_method()Output :-
This is the child method
Explanation:-
In this example, first we have defined a parent class with a my_method() method.
Next, the child class is defined which overrides the same method with its own implementation.
After that, an object of child class is created and my_method() is called using that object, then the child class method is executed.
This is how method overriding works in Python when a child class provides its own behavior for a parent class method.
Example 2:- Method Overriding in Python
class Report:
def generate(self):
print("Generating basic report")
class SalesReport(Report):
def generate(self):
super().generate()
print("Adding sales data to report")
report = SalesReport()
report.generate()Output:-
Generating basic report
Adding sales data to report
Explanation:-
When we create an object of SalesReport class and call generate() method, Python first looks for the generate() method in the child class.
Since the SalesReport class has its own generate() method, that is executed first.
Inside the child method, super().generate() method is called, so Python first executes the generate() method of parent class (Report).
Once the parent method finishes, Python sends control back to the child class method.
At last, child class adds its own logic and method execution is finished.
This example shows how method overriding with super() method allows a child class to extend parent behavior in a controlled and predictable way.
8. Method Overloading vs Method Overriding in Python
Method Overloading:-
Method overloading means defining multiple methods with the same name but different parameters.
Python doesn’t support traditional overloading like in Java or C++.
If multiple methods with the same name are defined, the last definition overrides earlier ones.
In Python language, you can achieve similar behavior with default arguments or variable length arguments.
Important line: Python does not perform compile-time method selection. The behavior is controlled at runtime inside the method body.
Example 1:- Simple Method Overloading
class A:
def print_data(self, data=None):
if data is None:
print("No data to print")
else:
print("Printing:", data)
obj = A()
obj.print_data()
obj.print_data("Report")Output :-
No data to print
Printing: Report
Explanation:-
In the above program, first we have created a class A.
Inside class A, we have defined a method print_data() with one parameter as data and its default value is set to None.
If we don’t pass any value, then default value data = None is used.
That is why first output is:
No data to print
In next method call, we pass the argument "Report", so the output becomes:
Printing: Report
In brief, in the above example, method behavior changes based on arguments.
As Python doesn’t support true method overloading, that is why we have simulated it using default parameters.
Let us now look at another practical example of method overloading using default arguments in Python.
This time, we will see how the same method can be used for logging messages with different log levels.
Example 2 :- Method Overloading
class Logger:
def log(self, message, level='INFO'):
print(f"[{level}] {message}")
obj = Logger()
obj.log("System started")
obj.log("Disk space low", "WARNING")Output :-
[INFO] System started
[WARNING] Disk space low
Explanation:-
In this example, log() method behaves differently based on the number of arguments.
This is a commonly used real-world pattern which is used to simulate method overloading in Python.
Senior developers prefer this approach and they don’t like writing multiple methods with the same name.
Note:-
See section 7 for Method Overriding.
Method Overloading vs Method Overriding in Python (Key Difference)
| Aspect | Method Overloading | Method Overriding |
| Definition | Method overloading refers to using the same method name with different parameters in the same class. | Method overriding occurs when a child class provides its own implementation of a parent class method. |
| Inheritance Requirement | It does not require inheritance in Python. | It always requires inheritance between parent and child classes. |
| Python Support | Python does not support traditional method overloading directly. | Python fully supports method overriding as part of OOP inheritance. |
| Method Signature | Parameters are different, but Python handles this through default or variable arguments. | Method name and parameters must remain the same as the parent method. |
| Runtime Behavior | Method selection depends on how arguments are handled inside the method. | Method selection happens at runtime based on the object type, enabling runtime polymorphism. |
| Real-World Usage | It is used occasionally to make methods flexible, but not considered core OOP in Python. | It is widely used in real Python applications, frameworks, and enterprise systems. |
9. Access Modifiers in Python
It is a very important topic in Python. Access modifiers in Python are used to control how class variables and methods are accessed from outside a class.
They help in three ways:
• data hiding
• security
• clean object-oriented design
If you want to write maintainable and professional Python code, then you must understand it.
Types of Access Modifiers in Python
Example 1: -
class Employee:
def __init__(self, name, salary, bonus):
self.name = name # public
self._salary = salary # protected
self.__bonus = bonus # private
emp = Employee("Amit", 40000, 5000)
# Accessing variables from outside the class
print(emp.name) # public access
print(emp._salary) # protected access
# print(emp.__bonus) # This will cause an errorOutput: -
Amit
40000
Explanation: -
In the above example, name is a public variable.
That is why emp.name is accessible from outside the class.
Next, _salary is a protected variable.
Python allows accessing it outside the class, but the single underscore is a warning to developers, which means that the protected variable is intended for internal use or subclass use.
Next, __bonus is a private variable, hence it can’t be accessed outside the class.
If you try to access it outside the class, it will show an error (AttributeError).
Example 2: -
class Employee:
def __init__(self, name, salary, bonus):
self.name = name # public
self._salary = salary # protected
self.__bonus = bonus # private
def show_details(self):
print("Name:", self.name)
print("Salary:", self._salary)
print("Bonus:", self.__bonus)
class Manager(Employee):
def show_salary(self):
print("Manager Salary:", self._salary)
# object creation
emp = Employee("Amit", 50000, 5000)
emp.show_details()
mgr = Manager("Rahul", 80000, 10000)
mgr.show_salary()Output: -
Name: Amit
Salary: 50000
Bonus: 5000
Manager Salary: 80000
Explanation: -
In this program, first we have created an Employee class with public, protected and private variables to demonstrate access modifiers in Python.
After that, Manager class inherits from Employee class (parent) and is able to access the protected variable _salary, but you cannot directly access the private variable __bonus.
Finally, we have created the objects of both classes and also called their methods to show the data.
10. Method Resolution Order (MRO) in Python
It defines the order in which Python looks for a method or attribute when it is called on an object.
It becomes very important to understand MRO when a class inherits from multiple parent classes.
Understanding MRO helps in three ways:
• It avoids confusion
• It prevents duplicate method calls
• It reduces hidden bugs in inheritance-based Python programs
Why MRO Exists (The Real Problem)
Based on my experience and understanding, MRO exists mainly to solve three practical problems in Python inheritance:
• To decide which method should be called first when multiple parent classes contain methods with the same name.
• To avoid confusion in complex inheritance structures like multiple and hybrid inheritance.
• To ensure a consistent and predictable order of method lookup so that logical bugs and duplicate method calls are prevented.
I have seen lots of bugs in production because developers don’t understand method lookup order.
Simple Example to Understand MRO
Example 1: -
class A:
def show(self):
print("Class A")
class B(A):
def show(self):
print("Class B")
class C(A):
def show(self):
print("Class C")
class D(B, C):
pass
obj = D()
obj.show()Output: -
Class B
In the above example, when obj.show() method is called, Python first looks for show() method in Class D.
Since there is no method defined in Class D, Python starts checking its parent classes from left to right, based on MRO rule.
So, the inheritance order in the above program is:
D → B → C → A → object
As class B appears before class C in the above shown inheritance order, so Python executes show() method from class B.
Important MRO Rules in Python
2. Child Before Parent Rule: -
3. No Duplicate Lookup Rule: -
4. C3 Linearization (consistent left-to-right order): -
11. When to Use Inheritance and When Not in Python
In the last couple of years, I got a chance to interact with junior developers and most of them think that inheritance should be used everywhere.
But based on my experience, I can confidently say that it should be used only when it clearly fits the design.
Python developers should use inheritance carefully and intentionally, not by default.
Now let us look at this topic from all perspectives.
a) You should use inheritance when there is a clear “Is-A” relationship between a child class and a parent class.
Example: -
I have mostly seen advanced inheritance usage while reading framework source code, not while writing simple apps.
a) If there is a “Has-A” relationship (not “Is-A”), then in such cases, composition is a better choice than inheritance.
12. Composition in Python
It is an object-oriented design principle where a class contains another class as an object and uses its functionality.
It represents a “has-a” relationship and helps keep classes loosely coupled.
Example: - Composition in Python
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self):
self.engine = Engine() # Car has an Engine
obj = Car()
obj.engine.start()Explanation: -
First, we define the Engine class and at this stage, no object is created.
At this time, Engine class has only one method start().
After that, we define Car class.
At this point, Python notes that whenever a Car class object is created, its __init__() method will run.
Next, we create an object (obj) of Car class and at this time, Python immediately calls the __init__() method of Car class.
Inside the __init__() method, self refers to Car object (obj) and Engine() creates a new Engine class object and that Engine object is stored inside Car object (obj) as engine.
So, you can say that car.engine refers to Engine class object.
Finally, when following statement is run:
obj.engine.start()
It calls the start() method of Engine object and start() method runs and prints the following message:
Engine started.
Engine started.
13. Common Mistakes Beginners Make in Inheritance (Python)
I have reviewed Python code written by beginners and junior developers and based on that experience, I can say confidently that most of the time inheritance bugs come not from syntax, but majority of times it comes from wrong design decisions.
Let me share these few common mistakes:
14. Best Practices for Inheritance in Production Systems
Based on my Python experience, I am going to share some best practices below that actually work in production systems.
Otherwise, if you (as a developer) do small inheritance mistakes, then it can create hard to debug and long-term issues.
You should always avoid deep inheritance chains with many levels, because it would increase mental load, debugging time and unexpected side effects.
Always remember that inheritance should model an “is-a” relationship, not just code reuse.
If you want to override methods (especially __init__()), then you should use super(), because if you skip it, then it can cause silent bugs in your code, as parent logic never runs.
You should contain core and stable behavior in parent classes.
One should consider a parent class as a contract – so you should be very careful while breaking it.
Inheritance behavior is often implicit and it is not obvious from reading code.
• What methods are meant to be overridden
If you won’t document inheritance behavior in a clear way, then it is like a technical debt.
15. Production-Grade Inheritance Example:
In big tech companies, I have seen systems fail not because of complex logic, but because every service followed a different execution flow.
Over time, this led to multiple issues:
If you use a BaseService inheritance pattern, then it enforces a single and reliable workflow and it also allows each service to focus only on its core responsibility.
class BaseService:
def __init__(self, service_name):
self.service_name = service_name
def execute(self):
print(f"Starting {self.service_name}")
self.process()
print(f"Finished {self.service_name}")
def process(self):
raise NotImplementedError("Subclasses must implement this method")
class EmailService(BaseService):
def __init__(self, service_name, recipient):
super().__init__(service_name)
self.recipient = recipient
def process(self):
print(f"Sending email to {self.recipient}")
class ReportService(BaseService):
def __init__(self, service_name, report_type):
super().__init__(service_name)
self.report_type = report_type
def process(self):
print(f"Generating {self.report_type} report")
# Object Creation and Execution
email_service = EmailService("Email Service", "admin@example.com")
email_service.execute()
report_service = ReportService("Report Service", "Sales")
report_service.execute()Output: -
Starting Email Service
Sending email to admin@example.com
Finished Email Service
Starting Report Service
Generating Sales report
Finished Report Service
Explanation: -
In this code, first we have defined a parent class named as BaseService, which represents a general service structure.
The __init__() method takes one parameter service_name and this value is stored inside self.service_name.
After that, we have defined an execute() method that defines a common workflow for all services. It performs three steps:
a) Prints: Starting service
b) Calls self.process()
c) Prints: Finished service
Note that process() method is intentionally incomplete and it raises an error:
“Subclasses must implement this method”
This is like an abstract method that forces child classes to implement their own logic.
The second-class name is EmailService which is a child class of BaseService (parent class).
Please note that this class inherits all methods from BaseService class.
Inside this child class, there is a constructor that takes two values as shown below:
def __init__(self, service_name, recipient):
super().__init__(service_name) method is used to call the base class parent class constructor to set service_name and self.recipient stores the email receiver.
You must note that this class adds extra data (recipient) on top of BaseService class.
Next, we have overridden process() method and this is the required implementation of process().
Now, it doesn’t raise an error, rather it prints:
Sending email to <recipient>
So, this is how overriding is done in real projects.
The third class name is ReportService which is also a child class of BaseService (parent class) and it also inherits everything from BaseService class.
This class also has a constructor __init__() that takes two values as shown below:
def __init__(self, service_name, report_type):
Again, super().__init__(service_name) is used to initialize parent part and self.report_type stores the type of report.
This child class has its own process() method version, so it prints:
Generating <report_type> report
Finally, we have created an object email of child class EmailService.
At this point, Python calls the __init__() method of EmailService and the constructor uses super() method to call the __init__() method of BaseService class and it finally initializes service_name and after that it initializes the recipient variable.
Next, we call email.execute() method, then Python runs the inherited execute() method which calls the overridden process() method of EmailService.
Similarly, we have created an object using:
report_service = ReportService("Report Service", "Sales")
and it also initializes service_name and report_type through __init__() method.
When we call report_service.execute() method, it runs the execute() method inherited from parent class (BaseService) and also calls the overridden process() method of ReportService (child class).
16. Frequently Asked Questions (FAQ) on Python Inheritance
Ans: -
Ans: -
Ans: -
Ans: -
Ans: -
17. Summary – Key Takeaways
If you use it correctly, then it organizes shared behavior and allows your systems to grow and it becomes easy to maintain as well.
If you master the following:
• parent and child classes
then you gain control over how real Python applications behave at runtime.
Learn more in our “Python Polymorphism" chapter.