Polymorphism is a programming concept that allows entities to be used in many different ways depending on the context in which it is being used. The name  itself is made up of the terms "poly" which means many, and "morphs" which means forms, thus it means having many forms.

Python uses polymorphism extensively for its various tasks. The obvious example is the  case with the builtin operators. For instance, if the plus( ) operator is given two numerical values such as integers or floats, it adds them together, but if it is given two sequence types such as strings, lists or tuples, it concatenates them.

print(4 + 5)
print("John" + "Doe")
print([1, 2, 3] + [4, 5, 6])
print(('Python', 'Java', 'PHP') + ('CSS', 'HTML', 'Javascript'))

Similarly, the asterisk operator( * ) performs multiplication If both operands are integers and repetition if one  operand is an integer and the other is a string, tuple, or list.

print(2 * "Python")
print(3 * [1, 2])
print(4 * (0.0,))

Polymorphism allows Python to easily adapt the behavior of the operators to different data types.

Polymorphism in Builtin Types

Polymorphism can be seen frequently in the built-in types. For example, when invoking the len() function  the result can differ depending on what type of data is being passed as an argument to the function. If the argument is a string, the result will be the length of that string, if a list, the result will be the length of that list, etc.

print(len([1, 2, 3, 4]))
print(len({"A", "B", "C", "D", "E"}))

Remember that the builtin builtin len()  function simply calls the __len__() method of its argument.

Polymorphism with custom classes

In Python classes, polymorphism is necessary when we want a method behave differently depending on the object that calls it. This can be  implemented using inheritance. The idea is to create a base class and then have multiple subclasses that all inherit from the class. The subclasses can then override the methods of the base class as required.

class Animal:
    def __init__(self, name):
         self.name = name
    #Define the polymorphed method
    def speak(self):
        raise NotImplementedError("Subclass must implement this abstract method")

class Dog(Animal):
    def speak(self):
        return "Woof"

class Cat(Animal):
     def speak(self):
          return "Purrr"

d = Dog("Tommy")

c = Cat("Kitty")



In the above case, the speak() method  behaves differently depending on which object called it. 

Note: The NotImplemented exception is a built-in exception that is raised when an abstract method that needs to be implemented in a derived class is not actually implemented.