5 Powerful Python Polymorphism Techniques You Need to Master: Polymorphic Code Example

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.

polymorphism examples in python
Taken from codegym

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.

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:

Hi, I’m Arup—a full-stack engineer at Enegma and a blogger sharing my learnings. I write about coding tips, lessons from my mistakes, and how I’m improving in both work and life. If you’re into coding, personal growth, or finding ways to level up in life, my blog is for you. Read my blogs for relatable stories and actionable steps to inspire your own journey. Let’s grow and succeed together! 🚀

Leave a Reply

Your email address will not be published. Required fields are marked *