In the previous lesson, you learned about Python inheritance, including parent and child classes, different types of inheritance, method overriding, and the use of super(). Now we move to another important concept of Object-Oriented Programming: polymorphism.
The word polymorphism comes from two ideas: “poly” meaning many and “morph” meaning forms. In programming, polymorphism means that the same interface, method, or operation can work with different types of objects and produce behavior appropriate to those objects.
In simple words:
Same interface
↓
Different objects
↓
Different behavior
This is one of the most powerful ideas in OOP because it allows us to write flexible code that can work with different objects without constantly checking their exact class.
Polymorphism allows different objects to respond to the same method call in their own way.
Consider an Animal example:
class Dog:
def speak(self):
print("Dog barks")
class Cat:
def speak(self):
print("Cat meows")
Both classes have a method called:
speak()
But the behavior is different.
A dog responds:
Dog barks
while a cat responds:
Cat meows
Now we can write a function that simply expects an object capable of performing speak():
def make_sound(animal):
animal.speak()
We can pass a dog:
dog = Dog()
make_sound(dog)
Output:
Dog barks
And we can pass a cat:
cat = Cat()
make_sound(cat)
Output:
Cat meows
The function does not need separate logic such as:
if object is Dog:
...
elif object is Cat:
...
It simply calls:
animal.speak()
The object itself determines which implementation runs.
Without polymorphism, code often becomes filled with type-specific conditions.
Imagine a program that processes different types of employees:
Each employee has a method called:
work()
Their work is different.
class Developer:
def work(self):
print("Writing code")
class Manager:
def work(self):
print("Managing the team")
class DataAnalyst:
def work(self):
print("Analyzing data")
Now we can create a general function:
def start_work(employee):
employee.work()
It works with all three objects:
developer = Developer()
manager = Manager()
analyst = DataAnalyst()
start_work(developer)
start_work(manager)
start_work(analyst)
Output:
Writing code
Managing the team
Analyzing data
The function does not need to know exactly which class it received.
This makes the code more flexible and easier to extend.
One of the most common ways polymorphism appears in Python is through method overriding.
You learned about overriding in the inheritance lesson.
Suppose we have:
class Animal:
def speak(self):
print("Animal makes a sound")
class Dog(Animal):
def speak(self):
print("Dog barks")
class Cat(Animal):
def speak(self):
print("Cat meows")
Now create objects:
dog = Dog()
cat = Cat()
Both objects support:
speak()
But each produces different behavior:
dog.speak()
cat.speak()
Output:
Dog barks
Cat meows
This is polymorphism because the same method call:
speak()
has different implementations for different objects.
Polymorphism becomes particularly useful when multiple classes inherit from the same parent.
class Employee:
def work(self):
print("Employee is working")
class Developer(Employee):
def work(self):
print("Developer is writing code")
class Manager(Employee):
def work(self):
print("Manager is managing the team")
class Analyst(Employee):
def work(self):
print("Analyst is analyzing data")
Now create a list:
employees = [
Developer(),
Manager(),
Analyst()
]
We can loop through the list:
for employee in employees:
employee.work()
Output:
Developer is writing code
Manager is managing the team
Analyst is analyzing data
This is a very important pattern.
The loop does not need to know whether the current object is a developer, manager, or analyst.
It simply assumes that the object supports:
work()
The appropriate method is selected based on the actual object.
A useful way to understand polymorphism is to think about an interface as a common contract.
Suppose every employee must have:
work()
The implementation can differ:
Developer → write code
Manager → manage team
Analyst → analyze data
But the calling code can remain the same:
employee.work()
This separation between the calling code and the specific implementation is one of the major benefits of polymorphism.
Polymorphism does not require a complex inheritance hierarchy.
Python can use polymorphism simply by writing functions that operate on objects supporting a particular operation.
For example:
class Dog:
def sound(self):
return "Bark"
class Cat:
def sound(self):
return "Meow"
def animal_sound(animal):
print(animal.sound())
Now:
animal_sound(Dog())
animal_sound(Cat())
Output:
Bark
Meow
The function works with both classes because both provide the required method.
This is one of the reasons Python is considered flexible when working with different object types.
An important point is that polymorphism in Python does not always require inheritance.
Consider:
class Car:
def start(self):
print("Car starts")
class Computer:
def start(self):
print("Computer starts")
These classes are completely unrelated.
There is no inheritance:
Car
Computer
But both provide:
start()
We can write:
def start_device(device):
device.start()
Then:
start_device(Car())
start_device(Computer())
Output:
Car starts
Computer starts
The function cares about what the object can do rather than what exact class it belongs to.
This idea leads directly to one of Python’s most important concepts related to polymorphism: duck typing, which will be explored in the next part.
Python’s built-in functions also demonstrate polymorphic behavior.
Consider the len() function.
We can use it with a string:
print(len("Python"))
Output:
6
We can use it with a list:
print(len([10, 20, 30, 40]))
Output:
4
We can use it with a tuple:
print(len((10, 20, 30)))
Output:
3
The same function:
len()
works with different types of objects.
The behavior depends on the object passed to the function.
This is another practical example of polymorphism in Python.
The + operator also behaves differently depending on the objects involved.
With integers:
print(10 + 20)
Output:
30
With strings:
print("Data " + "Analytics")
Output:
Data Analytics
With lists:
print([1, 2] + [3, 4])
Output:
[1, 2, 3, 4]
The same operator:
+
performs different operations depending on the operand types.
This behavior is related to another important OOP concept called operator overloading, which will be covered later in this lesson.
Polymorphism can also be useful when designing Data Analytics applications.
Imagine different report objects:
class SalesReport:
def generate(self):
print("Generating sales report")
class FinancialReport:
def generate(self):
print("Generating financial report")
class MarketingReport:
def generate(self):
print("Generating marketing report")
Now create a common processing function:
def generate_report(report):
report.generate()
We can pass different reports:
generate_report(SalesReport())
generate_report(FinancialReport())
generate_report(MarketingReport())
Output:
Generating sales report
Generating financial report
Generating marketing report
This design can be useful when an application supports multiple report types but wants to keep the calling code simple.
Suppose a dashboard application has different visualization objects.
class BarChart:
def display(self):
print("Displaying bar chart")
class LineChart:
def display(self):
print("Displaying line chart")
class PieChart:
def display(self):
print("Displaying pie chart")
We can create:
charts = [
BarChart(),
LineChart(),
PieChart()
]
Then:
for chart in charts:
chart.display()
Output:
Displaying bar chart
Displaying line chart
Displaying pie chart
The loop does not need separate conditions for each chart type.
This is exactly the kind of flexibility polymorphism can provide in larger software systems.
Without polymorphism, we might write:
def generate_report(report):
if isinstance(report, SalesReport):
print("Generating sales report")
elif isinstance(report, FinancialReport):
print("Generating financial report")
elif isinstance(report, MarketingReport):
print("Generating marketing report")
This works, but as the number of report types grows, the function becomes increasingly complicated.
With polymorphism:
def generate_report(report):
report.generate()
Each class takes responsibility for its own implementation.
This design is usually easier to extend. If a new report type is introduced, the new class can implement generate() without requiring the central function to be rewritten for every new type.
The main advantage can be summarized as:
Without Polymorphism
Function
↓
Check Type
↓
Choose Behavior
↓
Execute
With Polymorphism
Function
↓
Call Common Method
↓
Object Provides Behavior
The second approach can reduce dependencies between the calling code and individual classes.
Inheritance and polymorphism are related but they are not the same thing.
| Concept | Main Purpose |
|---|---|
| Inheritance | Reuse and extend functionality from another class |
| Polymorphism | Allow the same interface or operation to behave differently for different objects |
Inheritance can help create polymorphic relationships:
Animal
↓
Dog
Cat
Cow
where each child overrides speak().
But Python can also demonstrate polymorphism without inheritance:
Dog
Cat
can both implement:
speak()
and a function can work with either object.
Therefore, polymorphism is broader than inheritance.
len() demonstrate polymorphic behavior.+ can behave differently depending on the objects involved.You now understand the basic idea of polymorphism and how the same method or interface can produce different behavior for different objects. In the next part, we will explore the major forms of polymorphism in Python in more detail, especially method overriding, duck typing, operator overloading, and polymorphism with built-in functions.
Python supports polymorphism in several practical ways. The most common forms include method overriding, duck typing, operator overloading, and polymorphism through built-in functions.
Although these approaches work differently, they all follow the same basic idea: the code using an object does not always need to know its exact type. Instead, it can work with the behavior that the object provides.
For example, if different objects provide a method called display(), a function can simply call:
object.display()
without necessarily needing to know whether the object represents a student, employee, report, chart, or some other entity.
Method overriding occurs when a child class provides its own implementation of a method that already exists in the parent class.
Consider:
class Animal:
def speak(self):
print("Animal makes a sound")
class Dog(Animal):
def speak(self):
print("Dog barks")
class Cat(Animal):
def speak(self):
print("Cat meows")
Here, Animal defines a general speak() method.
The child classes redefine the same method:
Dog.speak()
Cat.speak()
Now create objects:
dog = Dog()
cat = Cat()
Call the same method:
dog.speak()
cat.speak()
Output:
Dog barks
Cat meows
The method name is identical, but the behavior depends on the object.
This is a classic example of polymorphism through inheritance.
Suppose we have several types of employees:
class Employee:
def work(self):
print("Employee is working")
class Developer(Employee):
def work(self):
print("Developer is coding")
class DataAnalyst(Employee):
def work(self):
print("Data Analyst is analyzing data")
class Manager(Employee):
def work(self):
print("Manager is managing the team")
We can place all three objects in a list:
employees = [
Developer(),
DataAnalyst(),
Manager()
]
Then use one loop:
for employee in employees:
employee.work()
Output:
Developer is coding
Data Analyst is analyzing data
Manager is managing the team
The loop does not contain any type-checking logic.
It does not ask:
Is this a Developer?
Is this a DataAnalyst?
Is this a Manager?
It simply asks each object to perform:
work()
Each object supplies its own implementation.
Duck typing is one of the most important ideas in Python programming.
The basic philosophy is often summarized as:
If it behaves like the required object,
it can be used as that object.
Python generally focuses on what an object can do rather than requiring it to belong to a particular class.
Consider:
class Dog:
def speak(self):
print("Dog barks")
class Cat:
def speak(self):
print("Cat meows")
These classes do not need to inherit from a common parent for the following function to work:
def make_sound(animal):
animal.speak()
Now:
make_sound(Dog())
make_sound(Cat())
Output:
Dog barks
Cat meows
The function does not check whether the object is a Dog or Cat.
It simply expects the object to provide:
speak()
This is duck typing.
The idea comes from a famous informal expression:
If it walks like a duck,
swims like a duck,
and quacks like a duck,
then it can be treated like a duck.
In programming terms, the important thing is the object’s behavior rather than its exact class identity.
For example, if a function requires an object with:
save()
then different objects can work with that function if they provide a compatible save() method.
class File:
def save(self):
print("Saving file")
class Database:
def save(self):
print("Saving database")
class CloudStorage:
def save(self):
print("Saving to cloud")
Now:
def store_data(storage):
storage.save()
We can use:
store_data(File())
store_data(Database())
store_data(CloudStorage())
Output:
Saving file
Saving database
Saving to cloud
The function works because all three objects provide the required behavior.
This example is important because there is no common parent class:
class PDFReport:
def generate(self):
print("Generating PDF report")
class ExcelReport:
def generate(self):
print("Generating Excel report")
class PowerBIReport:
def generate(self):
print("Generating Power BI report")
We can write:
def generate_report(report):
report.generate()
Now:
generate_report(PDFReport())
generate_report(ExcelReport())
generate_report(PowerBIReport())
Output:
Generating PDF report
Generating Excel report
Generating Power BI report
This is particularly useful in applications where unrelated classes can still provide the same behavior.
Another important form of polymorphism in Python is operator overloading.
We have already seen that the same operator can behave differently depending on the operands.
For example:
10 + 20
performs numerical addition.
But:
"Hello " + "Python"
performs string concatenation.
And:
[1, 2] + [3, 4]
combines two lists.
The operator remains:
+
but its behavior depends on the objects involved.
Python allows us to define how operators should behave with our own classes using special methods.
For example, the + operator can be customized using:
__add__()
Consider a simple Point class:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(
self.x + other.x,
self.y + other.y
)
Create two points:
point1 = Point(2, 3)
point2 = Point(4, 5)
Now:
point3 = point1 + point2
Python internally uses:
point1.__add__(point2)
The resulting point contains:
x = 2 + 4 = 6
y = 3 + 5 = 8
We can verify:
print(point3.x)
print(point3.y)
Output:
6
8
This allows custom objects to participate naturally in Python expressions.
Python provides many special methods for operator behavior.
| Operator | Special Method |
|---|---|
+ |
__add__() |
- |
__sub__() |
* |
__mul__() |
/ |
__truediv__() |
== |
__eq__() |
< |
__lt__() |
> |
__gt__() |
These methods are often called dunder methods, short for “double underscore methods.”
Let’s create a practical example.
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(
self.amount + other.amount
)
Create two objects:
money1 = Money(1000)
money2 = Money(2500)
Add them:
total = money1 + money2
Now:
print(total.amount)
Output:
3500
The expression:
money1 + money2
works because the class defines __add__().
Python’s len() function is another excellent example of polymorphism.
For a string:
text = "Python"
print(len(text))
Output:
6
For a list:
numbers = [10, 20, 30]
print(len(numbers))
Output:
3
For a dictionary:
student = {
"name": "Rahul",
"age": 21
}
print(len(student))
Output:
2
The same function works with different objects because those objects provide the behavior required by len().
We can also make our own class work with len().
class Course:
def __init__(self, lessons):
self.lessons = lessons
def __len__(self):
return len(self.lessons)
Now:
course = Course([
"Python",
"SQL",
"Excel",
"Power BI"
])
We can write:
print(len(course))
Output:
4
Python calls:
course.__len__()
internally.
Many Python functions operate on different object types.
For example, sorted() can work with different iterable collections:
numbers = [5, 2, 8, 1]
print(sorted(numbers))
Output:
[1, 2, 5, 8]
It can also work with strings:
letters = ["d", "a", "c", "b"]
print(sorted(letters))
Output:
['a', 'b', 'c', 'd']
The function remains the same while the data type can vary.
Imagine a data processing application with different data sources:
class CSVData:
def load(self):
print("Loading CSV data")
class ExcelData:
def load(self):
print("Loading Excel data")
class SQLData:
def load(self):
print("Loading SQL data")
Now create:
def process_data(source):
source.load()
print("Processing data")
We can use:
process_data(CSVData())
process_data(ExcelData())
process_data(SQLData())
Output:
Loading CSV data
Processing data
Loading Excel data
Processing data
Loading SQL data
Processing data
This is a useful design pattern for analytics applications because different data sources can expose a common operation such as load().
Suppose we have three objects that can export data:
class ExcelExporter:
def export(self, data):
print("Exporting data to Excel")
class CSVExporter:
def export(self, data):
print("Exporting data to CSV")
class PDFExporter:
def export(self, data):
print("Exporting data to PDF")
Instead of writing separate functions for every exporter, we can write:
def export_report(exporter, data):
exporter.export(data)
Now:
data = [10, 20, 30]
export_report(ExcelExporter(), data)
export_report(CSVExporter(), data)
export_report(PDFExporter(), data)
Output:
Exporting data to Excel
Exporting data to CSV
Exporting data to PDF
The function does not care about the specific exporter class. It only requires the expected behavior.
Duck typing encourages developers to focus on an object’s behavior rather than its identity.
Instead of thinking:
"What class is this object?"
we can think:
"What can this object do?"
This can make Python programs flexible and extensible.
A new class can often work with an existing function simply by implementing the expected method.
For example, if our application expects:
export()
we can create a new exporter:
class PowerBIExporter:
def export(self, data):
print("Exporting data to Power BI")
No change is required in:
export_report()
because the new class provides the expected behavior.
| Method Overriding | Duck Typing |
|---|---|
| Usually involves inheritance | Does not require inheritance |
| Child replaces or extends parent behavior | Objects simply provide expected behavior |
| Common parent interface | Behavior-based compatibility |
| Uses class hierarchy | Focuses on object capabilities |
Both approaches can support polymorphic behavior, but Python’s dynamic nature allows duck typing to work without requiring a formal inheritance relationship.
__add__() controls how the + operator behaves for a custom class.__len__() allows custom objects to work with len().len() demonstrate polymorphism.You now understand the major forms of polymorphism used in Python. In the next section, we will combine these ideas into practical applications, including polymorphism with functions, inheritance, duck typing, and examples related to real-world Data Analytics and software systems.
Polymorphism becomes especially useful when a program needs to work with different objects through a common interface. Instead of writing separate logic for every class, we can write general code that asks each object to perform an operation and lets the object provide its own implementation.
This approach is useful in applications such as reporting systems, data processing tools, payment systems, notification systems, dashboards, and many other software applications.
Consider a reporting application that can generate different types of reports.
class SalesReport:
def generate(self):
print("Generating Sales Report")
class FinancialReport:
def generate(self):
print("Generating Financial Report")
class MarketingReport:
def generate(self):
print("Generating Marketing Report")
All three classes provide the same method:
generate()
However, each class performs a different operation.
We can create a common function:
def generate_report(report):
report.generate()
Now we can pass different objects:
generate_report(SalesReport())
generate_report(FinancialReport())
generate_report(MarketingReport())
Output:
Generating Sales Report
Generating Financial Report
Generating Marketing Report
The function does not need to know which report class it received. It simply expects the object to provide a generate() method.
Data Analytics applications often work with multiple data sources. For example, data may come from CSV files, Excel files, databases, or APIs.
We can model this using polymorphism.
class CSVSource:
def load_data(self):
print("Loading data from CSV")
class ExcelSource:
def load_data(self):
print("Loading data from Excel")
class SQLSource:
def load_data(self):
print("Loading data from SQL database")
Now create a general processing function:
def process_data(source):
source.load_data()
print("Data processing started")
We can pass different sources:
process_data(CSVSource())
process_data(ExcelSource())
process_data(SQLSource())
Output:
Loading data from CSV
Data processing started
Loading data from Excel
Data processing started
Loading data from SQL database
Data processing started
This design allows the processing function to remain unchanged even if additional data sources are added later.
Suppose we later want to support an API.
class APIDataSource:
def load_data(self):
print("Loading data from API")
We can immediately use it:
process_data(APIDataSource())
Output:
Loading data from API
Data processing started
The process_data() function does not need to be modified.
This is one of the biggest benefits of polymorphism: new object types can often be introduced without changing existing processing logic.
A payment system is another good example.
Different payment methods may have different implementation details, but the application can expose a common operation:
pay()
For example:
class CreditCard:
def pay(self, amount):
print(
"Paid ₹",
amount,
"using Credit Card"
)
class UPI:
def pay(self, amount):
print(
"Paid ₹",
amount,
"using UPI"
)
class Cash:
def pay(self, amount):
print(
"Paid ₹",
amount,
"using Cash"
)
Now create a common function:
def process_payment(payment_method, amount):
payment_method.pay(amount)
We can use:
process_payment(CreditCard(), 500)
process_payment(UPI(), 500)
process_payment(Cash(), 500)
Output:
Paid ₹ 500 using Credit Card
Paid ₹ 500 using UPI
Paid ₹ 500 using Cash
The payment-processing function does not need separate if statements for every payment method.
Suppose an application can send notifications through different channels.
class EmailNotification:
def send(self, message):
print(
"Sending email:",
message
)
class SMSNotification:
def send(self, message):
print(
"Sending SMS:",
message
)
class WhatsAppNotification:
def send(self, message):
print(
"Sending WhatsApp message:",
message
)
We can write:
def notify(service, message):
service.send(message)
Now:
notify(
EmailNotification(),
"Your report is ready"
)
notify(
SMSNotification(),
"Your report is ready"
)
notify(
WhatsAppNotification(),
"Your report is ready"
)
The same function works with three different objects.
Again, the function depends on behavior rather than the exact class.
Polymorphism can also be implemented through a common parent class.
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
Now create objects:
rectangle = Rectangle(10, 5)
circle = Circle(7)
We can use the same method:
print(rectangle.area())
print(circle.area())
Output:
50
153.93791
Both objects provide:
area()
but the calculation is different.
Now suppose we have several shapes:
shapes = [
Rectangle(10, 5),
Circle(7),
Rectangle(4, 8)
]
We can calculate their areas with one loop:
for shape in shapes:
print(shape.area())
Output:
50
153.93791
32
The loop does not need to determine the type of every object.
It simply calls:
shape.area()
This is a classic polymorphic design.
Let’s create a slightly larger example related to Data Analytics.
Suppose an analytics company generates different reports:
Each report needs to perform two operations:
generate()
export()
We can create classes with those methods.
class SalesReport:
def generate(self):
print("Generating sales analysis")
def export(self):
print("Exporting sales report")
class CustomerReport:
def generate(self):
print("Generating customer analysis")
def export(self):
print("Exporting customer report")
class FinancialReport:
def generate(self):
print("Generating financial analysis")
def export(self):
print("Exporting financial report")
Now create a general workflow:
def process_report(report):
report.generate()
report.export()
We can process any compatible report:
process_report(SalesReport())
process_report(CustomerReport())
process_report(FinancialReport())
Output:
Generating sales analysis
Exporting sales report
Generating customer analysis
Exporting customer report
Generating financial analysis
Exporting financial report
The processing workflow remains the same even though the reports perform different tasks.
Polymorphism becomes especially useful when objects are stored together in a collection.
reports = [
SalesReport(),
CustomerReport(),
FinancialReport()
]
Now:
for report in reports:
report.generate()
report.export()
The same loop handles all report types.
This is much cleaner than writing separate loops for every report class.
Suppose the application later needs an InventoryReport.
We can create:
class InventoryReport:
def generate(self):
print("Generating inventory analysis")
def export(self):
print("Exporting inventory report")
Then simply add it to the collection:
reports.append(
InventoryReport()
)
The existing processing logic still works:
for report in reports:
report.generate()
report.export()
This demonstrates how polymorphism can support extensible software design.
Python also allows functions to accept objects that provide a required behavior.
For example:
def execute(operation, value):
return operation(value)
We can define different operations:
def square(value):
return value * value
def cube(value):
return value * value * value
Now:
print(execute(square, 5))
print(execute(cube, 5))
Output:
25
125
The same function:
execute()
works with different operations.
This is another example of Python’s flexible approach to polymorphism and callable objects.
Suppose an application supports multiple external services.
Each service provides:
fetch_data()
For example:
class WeatherAPI:
def fetch_data(self):
print("Fetching weather data")
class TourismAPI:
def fetch_data(self):
print("Fetching tourism data")
class SalesAPI:
def fetch_data(self):
print("Fetching sales data")
Now:
def fetch(api):
api.fetch_data()
We can use:
fetch(WeatherAPI())
fetch(TourismAPI())
fetch(SalesAPI())
The function remains independent of the individual API classes.
This type of design can be useful when integrating multiple data providers into an application.
Another practical example is file processing.
class CSVFile:
def read(self):
print("Reading CSV file")
class ExcelFile:
def read(self):
print("Reading Excel file")
class JSONFile:
def read(self):
print("Reading JSON file")
We can write:
def read_file(file):
file.read()
Then:
read_file(CSVFile())
read_file(ExcelFile())
read_file(JSONFile())
Again, the same function works with different objects.
Duck typing can sometimes cause runtime errors if an object does not provide the expected method.
For example:
class Student:
def study(self):
print("Student is studying")
class Car:
def drive(self):
print("Car is driving")
If we write:
def perform_action(obj):
obj.study()
then:
perform_action(Student())
works.
But:
perform_action(Car())
will fail because Car does not provide study().
This is one reason developers should clearly understand the expected interface or behavior when using duck typing.
In larger applications, developers may want to make the expected interface more explicit.
Python provides tools such as abstract base classes for this purpose.
For example:
from abc import ABC, abstractmethod
class Report(ABC):
@abstractmethod
def generate(self):
pass
Child classes can implement the required method:
class SalesReport(Report):
def generate(self):
print("Generating sales report")
This provides a more formal structure for polymorphic designs.
Abstract classes and abstract methods are an advanced OOP topic and will be useful when building larger Python applications.
Polymorphism provides several practical benefits:
if/elif checks for object types may be required.You now have a practical understanding of how polymorphism can be used to build flexible Python applications. The final section of this lesson will bring everything together with a complete project, coding exercises, common mistakes, interview questions, and a final revision of Python polymorphism.
In this lesson, you learned how polymorphism allows the same interface, method, function, or operator to work with different objects and produce different behavior. You also explored method overriding, duck typing, operator overloading, built-in function polymorphism, and practical applications in Data Analytics and software systems.
Let’s combine the concepts into a small Data Analytics report system.
Our system will support different report types:
Each report will provide two common operations:
generate()
export()
The implementation of these methods will be different for every report.
class SalesReport:
def generate(self):
print("Generating sales analysis")
def export(self):
print("Exporting sales report")
class FinancialReport:
def generate(self):
print("Generating financial analysis")
def export(self):
print("Exporting financial report")
class CustomerReport:
def generate(self):
print("Generating customer analysis")
def export(self):
print("Exporting customer report")
Now create a general processing function:
def process_report(report):
report.generate()
report.export()
We can pass any compatible report:
process_report(SalesReport())
process_report(FinancialReport())
process_report(CustomerReport())
Output:
Generating sales analysis
Exporting sales report
Generating financial analysis
Exporting financial report
Generating customer analysis
Exporting customer report
The important part is that process_report() does not check the class type. It simply expects the object to provide generate() and export().
Suppose the business later needs an inventory report.
class InventoryReport:
def generate(self):
print("Generating inventory analysis")
def export(self):
print("Exporting inventory report")
We do not need to modify:
process_report()
We can simply write:
process_report(InventoryReport())
Output:
Generating inventory analysis
Exporting inventory report
This demonstrates one of the biggest benefits of polymorphism: new compatible objects can be introduced without rewriting the existing processing logic.
We can also store all reports in a single list:
reports = [
SalesReport(),
FinancialReport(),
CustomerReport(),
InventoryReport()
]
Then:
for report in reports:
report.generate()
report.export()
The loop processes every object using the same interface.
There is no need to write:
if report is SalesReport:
...
elif report is FinancialReport:
...
elif report is CustomerReport:
...
The object itself provides the appropriate behavior.
The previous project used duck typing. Now let’s create a similar system using inheritance.
class Report:
def generate(self):
print("Generating report")
def export(self):
print("Exporting report")
Create child classes:
class SalesReport(Report):
def generate(self):
print("Generating sales analysis")
def export(self):
print("Exporting sales report")
class FinancialReport(Report):
def generate(self):
print("Generating financial analysis")
def export(self):
print("Exporting financial report")
class CustomerReport(Report):
def generate(self):
print("Generating customer analysis")
def export(self):
print("Exporting customer report")
Now:
reports = [
SalesReport(),
FinancialReport(),
CustomerReport()
]
Process them:
for report in reports:
report.generate()
report.export()
Here, all objects share the same parent class but override the parent’s methods.
This combines:
A classic OOP example is a shape system.
class Shape:
def area(self):
raise NotImplementedError(
"Child class must implement area()"
)
class Rectangle(Shape):
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
Now create different shapes:
shapes = [
Rectangle(10, 5),
Circle(7),
Square(6)
]
Calculate their areas:
for shape in shapes:
print(
"Area:",
shape.area()
)
Output:
Area: 50
Area: 153.93791
Area: 36
The same method:
shape.area()
produces different results depending on the actual object.
Let’s create a small class representing a data record count.
class DataCount:
def __init__(self, count):
self.count = count
def __add__(self, other):
return DataCount(
self.count + other.count
)
Create two objects:
dataset1 = DataCount(1000)
dataset2 = DataCount(2500)
Add them:
total = dataset1 + dataset2
Then:
print(total.count)
Output:
3500
The expression:
dataset1 + dataset2
works because the class defines:
__add__()
This is operator overloading.
Suppose we create a class representing a course:
class Course:
def __init__(self, lessons):
self.lessons = lessons
def __len__(self):
return len(self.lessons)
Create a course:
course = Course([
"Python",
"SQL",
"Excel",
"Power BI",
"Tableau"
])
Now:
print(len(course))
Output:
5
Python internally calls the object’s __len__() method.
This demonstrates how custom objects can integrate naturally with Python’s built-in functions.
Create an Animal parent class with a method:
speak()
Create:
DogCatCowOverride speak() in each child class.
Then create:
animals = [
Dog(),
Cat(),
Cow()
]
Use a single loop to call:
animal.speak()
Do not use if/elif statements to identify the animal type.
Create three classes:
UPICreditCardCashEach class should implement:
pay(amount)
Create a function:
process_payment(method, amount)
It should simply call:
method.pay(amount)
Test it with all three payment methods.
Create:
CSVSourceExcelSourceSQLSourceAPIDataSourceEvery class should implement:
load_data()
Create a function:
load(source)
that calls:
source.load_data()
Then process all four sources using the same function.
Create a Product class with:
name
price
Implement __add__() so that two product objects can be combined based on their prices.
For example:
product1 = Product("Laptop", 50000)
product2 = Product("Monitor", 20000)
total = product1 + product2
The resulting object should represent a total value of:
70000
Create a StudentGroup class that stores students in a list.
Implement:
__len__()
so that:
len(student_group)
returns the number of students.
1. Confusing inheritance with polymorphism
Inheritance is a mechanism for creating relationships and reusing behavior. Polymorphism is about using a common interface with different implementations.
2. Thinking polymorphism always requires inheritance
Python’s duck typing allows polymorphic behavior even between unrelated classes.
3. Checking every object type unnecessarily
If objects already provide a common method, excessive isinstance() checks can make code more complicated than necessary.
4. Forgetting the required interface
With duck typing, the object must actually provide the expected behavior. Otherwise, Python can raise an AttributeError at runtime.
5. Overusing operator overloading
Custom operator behavior should be intuitive. If + performs an unexpected operation, the code can become confusing.
6. Creating unrelated behavior under the same method name
Although polymorphism allows different implementations, those implementations should still represent a meaningful common operation.
1. What is polymorphism?
Polymorphism is the ability to use a common interface or operation with different objects and obtain behavior appropriate to each object.
2. What does polymorphism mean in OOP?
It means that the same interface can represent or trigger different implementations depending on the object.
3. How does method overriding support polymorphism?
A child class can override a parent method so that the same method call behaves differently for different child objects.
4. What is duck typing?
Duck typing is Python’s behavior-focused approach where an object can be used if it provides the required methods or operations, regardless of its exact class.
5. Does polymorphism require inheritance in Python?
No. Duck typing allows polymorphic behavior between unrelated classes.
6. What is operator overloading?
Operator overloading allows a class to define how operators such as +, -, or == behave for its objects.
7. Which method is used to overload +?
__add__()
8. Which method allows an object to work with len()?
__len__()
9. Give an example of polymorphism using a built-in function.
len() works with strings, lists, tuples, dictionaries, and other compatible objects.
10. What is the main advantage of polymorphism?
It allows common code to work with different objects, reducing unnecessary type-specific logic and making software easier to extend.
| Concept | Main Purpose |
|---|---|
| Encapsulation | Organize and control access to data and behavior |
| Inheritance | Reuse and extend functionality between classes |
| Polymorphism | Allow common interfaces to produce different behavior |
| Abstraction | Expose essential functionality while hiding implementation details |
These concepts often work together.
For example:
Employee
|
----------------
| | |
Developer Manager Analyst
| | |
work() work() work()
Inheritance creates the relationship.
Method overriding provides different implementations.
Polymorphism allows code to call:
employee.work()
without needing to know the exact employee type.
Polymorphism means “many forms”. In Python, it allows the same method, interface, function, or operator to work with different objects.
The most important forms covered in this lesson are:
len() can work with different compatible objects.A simple mental model is:
Different Objects
↓
Common Interface
↓
Different Implementations
↓
Polymorphic Behavior
For example:
Developer.work()
Manager.work()
Analyst.work()
All three can be accessed through:
employee.work()
even though the actual behavior is different.
In Data Analytics applications, this concept can be used for different data sources, report generators, exporters, visualization types, APIs, and processing systems.
The key principle is to design code around meaningful behavior rather than unnecessarily tying every function to a specific class.
With polymorphism understood, the major core OOP concepts are now coming together. The next lesson will move into another important Python OOP topic: Abstraction and Abstract Classes, where you will learn how to define common interfaces and hide unnecessary implementation details.