We're actively developing the Python course; advanced AI courses will be released soon.

Learn AI Way

Python Inheritance


1. Introduction


Python Inheritance is one of the core features of Object-Oriented Programming (OOP) that allows two things:

It allows a class to reuse and extend the behavior of another class.

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.

We will discuss:

five types of inheritance
method overriding
method overloading
real-world use cases
production concepts as well

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).
Next, you create a child class that inherits those things.
That child class can use, extend, or modify the parent class behavior.

Why Inheritance is Used

Before going deep into this topic, let’s understand the reason behind using inheritance.
Inheritance helps you in many ways:

1. Avoid Code Duplication
You write code only once
And reuse it many times

2. Improve Readability
Inheritance keeps your code organized and predictable, which improves readability.

3. Easy Code Maintenance
It becomes easy to maintain your code because you only do changes in the parent class, and it automatically affects all child classes.

4. Real-World Modeling
Inheritance helps you to model real-world relationships naturally.

3. Parent Class and Child Class (Core Concepts)


a) Parent Class (Base Class)
A parent class is a class that contains common code which can be reused by other classes.
It defines behavior that is shared by multiple related classes.

b) Child Class (Derived Class)
A child class is a class that inherits everything from the parent class.
It can add new features
It can change existing behavior
Without touching the parent class code

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.

Example: __init__() in Parent and Child Class

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.

Single Inheritance in Python

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.

Multiple Inheritance

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.

Multilevel Inheritance in Python

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.

Hierarchical Inheritance in Python

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.

Hybrid Inheritance in Python

Example: -

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

Explanation:-

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.

Always Remember:-

I have seen multiple times that some developers don’t understand this MRO flow.
Then it becomes very difficult for them to debug the code.

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.

Important Points About Method Overriding in Python:-

Method overriding in Python occurs only when inheritance is used. A child class must inherit from a parent class.
The name of the method and parameters must be the same in both parent and child classes.
When a method is overridden, the child class method gets priority during runtime method calls.
One can still access the parent class method using super(), if needed.
It supports runtime polymorphism in Python, which allows different behavior for the same method.
Method overriding is mainly used in frameworks, libraries, and enterprise-level Python systems to extend base functionality safely.

Method Overriding in Python

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)

AspectMethod OverloadingMethod Overriding
DefinitionMethod 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 RequirementIt does not require inheritance in Python.It always requires inheritance between parent and child classes.
Python SupportPython does not support traditional method overloading directly.Python fully supports method overriding as part of OOP inheritance.
Method SignatureParameters are different, but Python handles this through default or variable arguments.Method name and parameters must remain the same as the parent method.
Runtime BehaviorMethod 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 UsageIt 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


a) Public Access Modifier
One can access public members from anywhere in the program.
By default, all variables and methods are public in Python.

b) Protected Access Modifier
One can access protected members within the class and its child classes.
They are indicated using a single underscore ( _ ) prefix.

c) Private Access Modifier
One can access private members only within the class.
They are indicated using a double underscore ( __ ).

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 error

Output: -

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

Explanation: -

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.

How to Check MRO in Python
If you want to check MRO order in Python inheritance, then you can run the following statement:
print(D.mro())

Output :-
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

Important MRO Rules in Python


1. Left-to-Right Rule: -
In multiple inheritance, Python checks parent classes from left to right (as written in class definition).

2. Child Before Parent Rule: -
Python will always check a child class before its parent class while searching for a method.

3. No Duplicate Lookup Rule: -
Each class appears only once in the MRO order, even if it is inherited through multiple paths.

4. C3 Linearization (consistent left-to-right order): -
Python follows C3 linearization, which ensures a consistent method lookup order where child classes are checked first, followed by parent classes from left to right, without repeating any class.

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.

When You Should Use Inheritance in Python

a) You should use inheritance when there is a clear “Is-A” relationship between a child class and a parent class.

Example: -
A Manager is an Employee
This helps to maintain logical consistency and clean class hierarchies in real Python projects.

b) You should use inheritance when multiple classes share common behavior and you can write that behavior in one place.
In my experience, this is very helpful in large Python-based applications where logic evolves over time.

c) Inheritance is useful when child classes need to override or extend the parent class behavior.
Method overriding is commonly used in:
Python frameworks
Plugin systems
Extensible architectures
You should always remember one thing that method overriding allows customization without modifying existing code.

d) Inheritance is often used in frameworks to provide base classes to which users can extend. This helps to maintain consistency.

I have mostly seen advanced inheritance usage while reading framework source code, not while writing simple apps.

When You Should NOT Use Inheritance in Python

a) If there is a “Has-A” relationship (not “Is-A”), then in such cases, composition is a better choice than inheritance.

b) If inheritance is making your code hard to understand, then we shouldn’t use inheritance.

c) If you want to reuse just one or two methods, then in those scenarios, composition is a better choice.

d) If your child class constantly overrides most of the parent logic, then inheritance becomes fragile and this often leads to unpredictable behavior and bugs in the system.

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.

Output: -

Engine started.

Important Points About Composition: -

a) It promotes loose coupling, so it is easier to modify and maintain the code.
b) If you perform changes in one class, then those changes don’t directly affect the other class.
c) It is widely used in production-level Python applications.
d) It makes testing and debugging easier. You can mock, stub or swap internal objects without touching the outer class.
e) Composition allows you to replace, wrap or extend internal objects without rewriting class hierarchies, so it protects you from future requirement changes.

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:

a) I have seen many developers use inheritance only to reuse one or two methods.
This creates a weak relationship between classes and tightly couples them.
If you only need a functionality, then you should use composition, as it is the better choice.

b) Most of the time, freshers override the child constructor and they forget to call the parent class constructor.
Due to this mistake, parent attributes never get initialized and this leads to silent runtime bugs.
And these types of bugs are very hard to trace because your code still runs.

c) Many times, freshers keep on adding hierarchies (without any need) and it becomes harder to understand, test and debug the code.
Normally, 2–3 levels of inheritance are usually enough in real projects.

d) Sometimes developers override a method and they completely ignore the parent class behavior and this becomes risky as well.
So, if a parent behavior matters, then child should call super() method.

e) Protected variables are accessed from anywhere on frequent basis, but protected members are meant for internal and subclass use and this leads to fragile designs.

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.

a) Keep Inheritance Hierarchies Shallow: -

You should always avoid deep inheritance chains with many levels, because it would increase mental load, debugging time and unexpected side effects.

b) Prefer Composition When Possible: -

Always remember that inheritance should model an “is-a” relationship, not just code reuse.
I have seen that composition scales better (in production systems) than inheritance.

c) Always Use super() Correctly: -

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.

d) Keep Parent Classes Simple and Stable: -

You should contain core and stable behavior in parent classes.
It should never contain frequently changing logic, because if you change parent class logic frequently, then it will affect all child classes.

One should consider a parent class as a contract – so you should be very careful while breaking it.

e) Document Inheritance Behavior Clearly: -

Inheritance behavior is often implicit and it is not obvious from reading code.
As a best practice, you should always document following:

What methods are meant to be overridden
Whether super() method must be called
What assumptions child classes should follow

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:
duplicated code
inconsistent behavior
hard to debug in production

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.

Example: Base Service and Specialized Services

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


1. What is inheritance in Python with a real-world example?

Ans: -
Inheritance in Python is an object-oriented feature where a child class reuses and extends the behavior of a parent class.
For example, you are working in a Payment System and there is a pay() method inside a Payment base class and child classes like CreditCardPayment implement their own payment logic.
This allows different payment methods to share a common structure while behaving differently.
It reduces code duplication and makes system easier to extend as well.

2. What are different types of inheritance in Python with examples?

Ans: -
Python supports 5 types of inheritance:
Single
Multilevel
Hierarchical
Multiple
Hybrid
Single inheritance involves one parent and child class.
Multilevel inheritance forms a chain of classes.
Hierarchical inheritance allows multiple child classes to inherit from one parent class.
In multiple and hybrid inheritance, behavior from more than one parent class is combined and they are used carefully in advanced systems.

3. What is super() method in Python and why is it used?

Ans: -
In Python, we use super() method to call methods from a parent class inside a child class.
super() method ensures that parent logic is executed completely (especially during object initialization time).
If you use super() method, then it makes inheritance safer, cleaner and more maintainable in production.

4. What is the difference between method overloading and method overriding in Python?

Ans: -
Method Overriding: -
In this, a child class provides its own implementation of a method defined in the parent class.
Method Overloading: -
In this, you define multiple methods with same name but different parameters.
It is not directly supported in Python.
Instead, you use default arguments to simulate overloading behavior in Python.

5. When should inheritance be used and when should it be avoided in Python?

Ans: -
You should use inheritance only when there is a clear “is-a” relationship between the parent class and child class.
If you can say that child class is a specialized form of parent class, then inheritance is the right choice.
You should avoid inheritance when the relationship is “has-a” or when behavior needs to change dynamically.
In such scenarios, you should use composition, as it provides better flexibility and simpler design.

17. Summary – Key Takeaways

Python inheritance is a core concept of object-oriented programming in Python that helps developers in many ways like to build clean, reusable and scalable code.

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
proper use of __init__() and super()
different types of inheritance in Python
method overriding

then you gain control over how real Python applications behave at runtime.

Most importantly, you should use inheritance intentionally and only when there is a clear “is-a” relationship.
If you understand and apply these principles, it will help you to become a professional Python developer.

Learn more in our “Python Polymorphism" chapter.