Polymorphism is a core concept in object-oriented programming (OOP) that enhances code readability and maintainability. In Python, polymorphic code appears in various scenarios, whether related to OOP or functional programming.
Understanding polymorphism in Python is essential, as it offers unique flexibility compared to other languages.
It’s important to know how different forms of polymorphism can be applied to write cleaner and more efficient code.

In this blog, we’ll explore the types of polymorphism in Python with clear examples for each. These examples will demonstrate how polymorphism helps developers manage and maintain large codebases effectively.
Read on to learn how to use polymorphism in Python and why it’s a valuable tool for building scalable applications.
What Will You Learn?
6 Examples of Polymorphism in Python
Polymorphism is a core concept in object-oriented programming (OOP) that enhances code readability and maintainability. In Python, polymorphism appears in various scenarios, whether related to OOP or functional programming.
Polymorphism simply means “many forms.” It allows objects to behave differently based on the context. Think of yourself:
- At home, you’re a son or daughter.
- At school or work, you’re a student or employee.
- On the field, you’re a player.
You act, speak, and respond differently depending on the situation. This flexibility is the essence of polymorphism.
Here, we’ll cover:
- Polymorphism in Built-In Functions: len, reversed, etc.
- Method Overriding
- Polymorphism in Class Methods
- Polymorphism in Functions and Objects
- Method Overloading
- Operator Overloading
Read on to learn how to use polymorphism in Python and why it’s a valuable tool for building scalable applications.
Built-In Functions
Python has built-in functions like len
and reversed
that behave differently depending on the type of data they are used with.
For example:
user = "Arup"
print(len(user))
users = ["Arup", "Jay", "Surendar"]
print(len(users))
country = "India"
r_country = ''.join(reversed(country))
print(r_country)
countries = ["India", "US", "Japan"]
r_countries = list(reversed(countries))
print(r_countries)
Output:
4
3
aidnI
['Japan', 'US', 'India']
When you use len
on a string, it returns the number of characters in that string. When used on a list, it gives the number of items in the list. Similarly, reversed
works differently based on the type of data it operates on.
This behavior is an example of polymorphism—the ability of a single interface (like a function) to work with different types of data.
Next, let’s look at the most common form of polymorphism: method overriding.
Polymorphism with method overriding
Most object-oriented languages support method overriding, which is one of the primary forms of polymorphic code. It’s often the first example that comes to mind when discussing polymorphism.
Method overriding works in combination with inheritance. First, you inherit a parent class containing a method you want to modify. Then, in the child class, you redefine that method to provide specific functionality. When you call the method on an instance of the child class, the overridden method is executed instead of the one in the parent class.
class Authenticator:
def authenticate(self, username, password):
print(f"Authenticating user: {username}")
if username == "user123" and password == "password123":
print("Authentication successful!")
return True
else:
print("Authentication failed!")
return False
class OTPAuthenticator(Authenticator):
def authenticate(self, username, password, otp):
if not super().authenticate(username, password):
return False
print("Verifying OTP...")
if otp == "123456":
print("OTP verified. Login successful!")
return True
else:
print("Invalid OTP. Authentication failed!")
return False
print(Authenticator().authenticate("user123", "password123"))
'''
Output:
Authenticating user: user123
Authentication successful!
True
'''
print(OTPAuthenticator().authenticate("user123", "password123", "1221"))
'''
Output:
Authenticating user: arup@thestartupcoder.com
Authentication failed!
False
'''
In the example, the Authenticator
class provides a general authenticate method. The OTPAuthenticator
class extends this functionality by adding an additional OTP verification step. As seen, the authentication passes for Authenticator
when the username
and password
are correct. However, it fails for OTPAuthenticator
because the otp
is incorrect.
Here, the authenticate
method of the Authenticator
class is overridden in OTPAuthenticator
to include the extra OTP validation layer.
Now you know how to override methods in your classes to extend or modify existing functionality.
Want to see how to achieve polymorphism using built-in functions like len
for your custom data types? Let’s dive into it next!
Polymorphism With Built-In Function
How can you use built-in functions like len()
with your custom data types? The answer lies in implementing dunder methods like __len__
.
class ImageDataset:
def __init__(self, data):
self.data = data
def __len__(self):
imgs = [img for img in self.data if img.endswith(('.jpg', '.png'))]
return len(imgs)
files = ["0.txt", "1.txt", "bird.jpg", "boy.png", "tree.png"]
dataset = ImageDataset(files)
print(len(dataset))
Output:
3
In this example, I’m creating a class called ImageDataset
to manage a collection of images. This class holds image data and provides functionality to perform operations on it. For now, we’re focusing on the ability to get the length of the dataset.
Within the __len__
method, I’ve included logic to filter the dataset, considering only image files, and then return the length of that subset.
Another example of polymorphic code in action is when you need to handle different payment methods. Instead of writing separate logic for each method, polymorphism allows you to process each payment type according to its own specific processing rules.
Polymorphism In Class Methods
In various scenarios, you may need to execute multiple tasks in a workflow pipeline (e.g., data validation, transformation, API calls), or you might want to send notifications through different channels, each with its own method of communication. In such cases, polymorphic code is the key to achieving different behaviors using the same object.
For example, consider a payment processing system where you have multiple transaction methods, and you need to process a list of payments for each specific transaction type:
class PaymentMethod:
def process_payment(self, amount):
raise NotImplementedError("Need to implement process_payment")
class CreditCardPayment(PaymentMethod):
def process_payment(self, amount):
print(f"Processing credit card payment of ${amount:.2f}.")
class PayPalPayment(PaymentMethod):
def process_payment(self, amount):
print(f"Processing PayPal payment of ${amount:.2f}.")
class BankTransferPayment(PaymentMethod):
def process_payment(self, amount):
print(f"Processing bank transfer payment of ${amount:.2f}.")
payments = [
CreditCardPayment(),
PayPalPayment(),
BankTransferPayment(),
CreditCardPayment()
]
# Process payments with different methods
amounts = [100.50, 200.75, 150.00, 300.25]
print("Processing all payments:")
for payment, amount in zip(payments, amounts):
payment.process_payment(amount)
Output:
Processing all payments:
Processing credit card payment of $100.50.
Processing PayPal payment of $200.75.
Processing bank transfer payment of $150.00.
Processing credit card payment of $300.25.
You can create a function that accepts an object and calls a method on that object. The method being called is a common one that the function assumes is always valid, regardless of the specific object type.
The next point will cover a polymorphic code example, illustrating how functions and objects can interact seamlessly.
Polymorphic Code Example – Functions and Objects
This scenario is often referred to as duck typing in Python. In essence, it means you don’t need to know the exact type of an entity, as long as it behaves as expected—in this case, if it can “quack”, it must be a duck.
class CreditCardPayment():
def process_payment(self, amount):
print(f"Processing credit card payment of ${amount:.2f}.")
class PayPalPayment():
def process_payment(self, amount):
print(f"Processing PayPal payment of ${amount:.2f}.")
class BankTransferPayment():
def process_payment(self, amount):
print(f"Processing bank transfer payment of ${amount:.2f}.")
payments = [
CreditCardPayment(),
PayPalPayment(),
BankTransferPayment(),
CreditCardPayment()
]
amounts = [100.50, 200.75, 150.00, 300.25]
def processing_payment(obj, amount):
obj.process_payment(amount)
print("Processing all payments:")
for payment, amount in zip(payments, amounts):
processing_payment(payment, amount)
Output:
Processing all payments:
Processing credit card payment of $100.50.
Processing PayPal payment of $200.75.
Processing bank transfer payment of $150.00.
Processing credit card payment of $300.25.
We get the same result here, but we’re using a function that assumes every obj
will have the process_payment
method, regardless of its actual type.
Looking to overload Python functions like you would in C++ or Java? Python handles this differently, which we’ll explore next when we discuss method overloading.
Method Overloading
Python does not natively support method overloading, but you can implement it in a way that mimics this behavior.
Take the range
function, for example. Have you ever wondered how it can accept different numbers of arguments and behave differently depending on the sequence provided? This is Python’s way of achieving method overloading.
class Shape:
# function with two default parameters
def area(self, a, b=0):
if b > 0:
print('Area of Rectangle is:', a * b)
else:
print('Area of Square is:', a ** 2)
square = Shape()
square.area(5)
rectangle = Shape()
rectangle.area(5, 3)
Output:
Area of Square is: 25
Area of Rectangle is: 15
Next, we’ll dive into operator overloading.
Operator Overloading
Python allows operator overloading through the use of dunder methods such as __add__
, __sub__
, and others.
Let’s build a Vector class that behaves like a mathematical vector. For now, we’ll implement the addition functionality:
class Vector:
def __init__(self, *components):
self.components = list(components)
def __str__(self):
return f"Vector({self.components})"
def __add__(self, op_vector):
if len(op_vector.components) != len(self.components):
raise ValueError("Vectors must have the same dimensions for addition.")
results = []
for c1, c2 in zip(self.components, op_vector.components):
results.append(c1+c2)
return Vector(*results)
vec1 = Vector(1, 2, 3)
vec2 = Vector(2, 3, 4)
print(vec1+vec2)
Output:
Vector([3, 5, 7])
With this implementation, you can now add two vectors together!
Conclusion
In this blog, we’ve explored various aspects of polymorphic code in Python, highlighting it enables flexible and dynamic behavior in your code.
We started by discussing how polymorphism can be applied to built-in functions like len
and reversed
, and then moved on to method overriding and its practical applications.
We also covered polymorphism in class methods and functions, demonstrating how objects can behave differently based on their types.
We touched on method overloading, explaining how Python handles it differently than languages like C++ or Java, and concluded with operator overloading, showing how dunder methods like __add__
and __sub__
allow you to define custom behavior for operators.
If you’re interested in diving deeper into Python and other advanced concepts, you might want to start out from my blog on “Master the Basics: 9 Python Concepts List You Can’t Ignore“, where we further explore many more techniques in Python.
Thank you for reading, and happy coding! 👋
References: