Polymorphism allows objects of different types, each with their own specific behaviours, to be treated as the same general type.
The Advantages are
- Flexible code for different object types, promoting code reuse and modularity.
- Concise and readable code by abstracting common behaviours across objects.
- Easy maintenance by treating new object types uniformly based on shared behaviour.
- Dynamic behaviour at runtime for dynamic and flexible programming.
- Duck typing allows treating objects based on behaviour, enhancing versatility.
- In Python, polymorphism is achieved through method overriding and duck typing.

Example:
- All Shape objects have an x, y coordinate (with corresponding getter and setter methods).
- In addition, Shape objects can also calculate their areas.
- How a shape’s area is computed, however, depends on what shape it is.
- Thus, it is not possible to define a calcArea method in the Shape class that serves the purposes of all types of shapes.
- On the other hand, we want all shape types to have a calcArea method.
- Therefore, we add an unimplemented version of the calcArea method to the Shape class as shown

- Subclasses of the Shape class must implement the calcArea method, otherwise a Not ImplementedError exception is raised.
- An abstract class is a class that cannot be instantiated directly. It often contains one or more abstract methods, which are methods without an implementation. It serves as a blueprint for other classes and defines a common interface that its subclasses must implement.

- The __init__ method of each subclass first calls the __init__ method of the Shape class with the x and y coordinates to set the location of the shape.
- This allows the Shape class to maintain the x and y values for all shapes.
- However, the way size is handled differs among the subclasses.
- In the Circle class, the size is stored as the radius.
- In the Square and Triangle classes, the size is stored as the length of each side.
- Creating shape objects: Several instances of Circle, Square, and Triangle objects are created with different coordinates and sizes.
# Creating shape objects
circle1 = Circle(0, 0, 5)
square1 = Square(1, 1, 4)
triangle1 = Triangle(3, 3, 6)
Suppose that there was a list of Shape objects for which the total area of all shapes combined was to be calculated,
# Calculating the total area of all shapes
shapes_list = [circle1, square1, triangle1]
total_area = 0
for shape in shapes_list:
total_area += shape.calcArea()
print("Total area of all shapes:", total_area)
In this code, shapes_list is a list that contains different shape objects such as circles, squares, and triangles. The variable total_area is initialized to 0, and then a loop is used to iterate over each shape in the list. For each shape, the calcArea() method is called to calculate its area, and the result is added to the total_area variable. Finally, after iterating over all the shapes, the total_area will contain the combined area of all the shapes in the list.
In Python, it is not because Circle, Square, and Triangle are subclasses of the Shape class that allows them to be treated in a similar way. It is because the classes are subtypes of a common parent class. In Python, any set of classes with a common set of methods, even if not subclasses of a common type, can be treated similarly. This kind of typing is called duck typing— that is, “if it looks like a duck and quacks like a duck, then it’s a duck.”