Python Polymorphism Explained for Beginners | OOP Complete Guide



Circleπ × r²Rectanglel × wTriangle½ × b × hSquares × sPYTHON OOPPythonPolymorphismmethod overriding · duck typing · operator overloadingcodewithpypy.blogspot.com

Python Polymorphism – One Name, Many Forms Explained for Beginners

📅 Published on Code with Py📂 Category: Python OOP⏱️ Reading time: ~14 min

You already know how to build classes and use inheritance to share code between them. Now here is a question — what if ten different classes all need a method called area()? Every shape calculates area differently, but you want to call area() the same way on all of them without caring which shape it is. That is exactly what Polymorphism solves. The word comes from Greek — poly means many, morph means form. Same method name, different behaviour depending on which object uses it. This guide breaks it down completely with clear examples and real-life reasoning.

📋 Table of Contents

  1. What Exactly Is Polymorphism?
  2. Real-Life Analogy — The Same Switch, Different Lights
  3. Polymorphism With Functions (Built-in Examples)
  4. Polymorphism With Classes — Method Overriding
  5. Step-by-Step Trace — How Python Picks the Right Method
  6. Polymorphism With a Common Interface
  7. Duck Typing — Python's Unique Approach
  8. Operator Overloading — Same Operator, Different Behaviour
  9. Abstract Classes — Enforcing Polymorphism
  10. Polymorphism vs Inheritance — Key Differences
  11. Real-Life Project
  12. Practice Problems
  13. FAQ
  14. Summary

1. What Exactly Is Polymorphism?

Here is the core idea in one sentence — polymorphism lets you call the same method name on different objects and get different results based on which object is actually doing the work.

Think about a remote control. You press the power button and it turns on the TV. Your friend presses the same power button on a different remote and it turns on an AC. Same button name — power — completely different actions depending on which device is receiving the signal.

In Python, polymorphism shows up in three main ways — through method overriding in inherited classes, through duck typing, and through operator overloading. You will see all three clearly in this post.

Without Polymorphism

You call circle_area(), rectangle_area(), triangle_area() separately for each shape. More functions, more names to remember, harder to loop through mixed shapes.

With Polymorphism

You call area() on any shape — Python figures out which version to run. One loop handles every shape. Code stays clean no matter how many shapes you add.


2. Real-Life Analogy — The Same Switch, Different Lights

💡 One Action, Many Reactions

In your house, you have a single habit — flick the switch upward to turn on the light. In the bedroom, flicking up turns on a warm dim light. In the kitchen, the same action turns on bright white light. In the bathroom, it turns on both light and exhaust fan together.

You performed one action — flick switch — but each room responded differently based on what was installed there. That is polymorphism. In Python, you call one method name, and each object responds in its own way based on how it was defined.


3. Polymorphism With Functions — Built-in Examples

You have already used polymorphism without knowing it. Python's built-in functions like len(), +, and print() are polymorphic — they behave differently depending on what type of data you pass them.

# len() behaves differently for each type
print(len("Python"))       # String — counts characters
print(len([1, 2, 3, 4]))   # List — counts items
print(len({"a": 1, "b": 2})) # Dict — counts keys
print(len((10, 20, 30)))    # Tuple — counts items
6
4
2
3
# + operator is also polymorphic
print(10 + 20)               # Addition for integers
print(3.5 + 1.5)             # Addition for floats
print("Hello" + " World")    # Concatenation for strings
print([1, 2] + [3, 4])      # Merge for lists
30
5.0
Hello World
[1, 2, 3, 4]
Same + operator, four completely different behaviours — all based on what type of data is on each side. This is polymorphism at the language level, and it is why Python feels so natural to write.

4. Polymorphism With Classes — Method Overriding

When you have multiple classes with the same method name — and each class has its own version of that method — calling the method gives a different result depending on which object you called it on. This is the most common form of polymorphism in OOP.

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

    def describe(self):
        print(f"Circle with radius {self.radius}")


class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width  = width

    def area(self):
        return self.length * self.width

    def describe(self):
        print(f"Rectangle {self.length}x{self.width}")


class Triangle:
    def __init__(self, base, height):
        self.base   = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

    def describe(self):
        print(f"Triangle base={self.base} height={self.height}")


# Polymorphism — same call, different result
shapes = [Circle(7), Rectangle(5, 4), Triangle(6, 8)]

for shape in shapes:
    shape.describe()
    print(f"Area: {shape.area():.2f}\n")
Circle with radius 7
Area: 153.94

Rectangle 5x4
Area: 20.00

Triangle base=6 height=8
Area: 24.00
The loop does not know or care whether it is processing a Circle, Rectangle, or Triangle. It just calls .area() on whatever object it finds — and Python handles the rest. This is the power of polymorphism.

5. Step-by-Step Trace — How Python Picks the Right Method

Trace of: for loop calling shape.area() on shapes = [Circle(7), Rectangle(5,4), Triangle(6,8)]
1
Loop iteration 1: shape = Circle(7) object
2
shape.area() → Python checks: what type is shape? → Circle
3
Calls Circle.area(self) → returns 3.14159 × 7² = 153.94
4
Loop iteration 2: shape = Rectangle(5,4) object
5
shape.area() → Python checks: what type is shape? → Rectangle
6
Calls Rectangle.area(self) → returns 5 × 4 = 20
7
Loop iteration 3: shape = Triangle(6,8) object
8
shape.area() → Python checks: what type is shape? → Triangle
Calls Triangle.area(self) → returns 0.5 × 6 × 8 = 24.00

6. Polymorphism With a Common Interface (Using Inheritance)

A very clean way to guarantee polymorphism is to have all classes inherit from one common parent. The parent defines what methods must exist. Each child defines how they work.

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("Every animal must define speak()")

    def move(self):
        raise NotImplementedError("Every animal must define move()")


class Dog(Animal):
    def speak(self):
        print(f"{self.name} says: Woof Woof!")

    def move(self):
        print(f"{self.name} runs on four legs.")


class Bird(Animal):
    def speak(self):
        print(f"{self.name} says: Tweet Tweet!")

    def move(self):
        print(f"{self.name} flies through the air.")


class Fish(Animal):
    def speak(self):
        print(f"{self.name} is silent — fish don't speak!")

    def move(self):
        print(f"{self.name} swims through the water.")


# Polymorphism — all animals, one loop
animals = [Dog("Bruno"), Bird("Tweety"), Fish("Nemo")]

for animal in animals:
    animal.speak()
    animal.move()
    print()
Bruno says: Woof Woof!
Bruno runs on four legs.

Tweety says: Tweet Tweet!
Tweety flies through the air.

Nemo is silent — fish don't speak!
Nemo swims through the water.

7. Duck Typing — Python's Unique Approach to Polymorphism

Python has a famous saying — "If it walks like a duck and quacks like a duck, it's a duck." This means Python does not care what type an object actually is. It only cares whether the object has the method you are trying to call. If it does — great, it works. If not — you get an error.

This is different from languages like Java where you need to formally declare that a class implements an interface. Python trusts you to pass the right objects.

# These three classes have NO inheritance relationship
class Printer:
    def produce(self):
        print("Printer: Printing a document...")

class Camera:
    def produce(self):
        print("Camera: Capturing a photograph...")

class MusicPlayer:
    def produce(self):
        print("Music Player: Playing a song...")

# Duck typing — Python only checks if produce() exists
def activate(device):
    device.produce()   # Works on ANY object with produce()

devices = [Printer(), Camera(), MusicPlayer()]

for d in devices:
    activate(d)
Printer: Printing a document...
Camera: Capturing a photograph...
Music Player: Playing a song...
Printer, Camera, and MusicPlayer share zero inheritance. Python does not care — as long as each has a produce() method, the activate() function works perfectly on all of them. This flexibility is what makes Python so expressive.

8. Operator Overloading — Same Operator, Different Behaviour

You can teach Python what to do when standard operators like +, -, *, ==, and > are used on your custom objects. This is done through special dunder methods (double underscore methods).

class Vector:
    """Represents a 2D mathematical vector."""
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        # What happens when you use + between two Vectors
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        # Multiply a vector by a number
        return Vector(self.x * scalar, self.y * scalar)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __str__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(v1 + v2)        # calls __add__
print(v1 - v2)        # calls __sub__
print(v1 * 3)         # calls __mul__
print(v1 == v2)       # calls __eq__
print(v1 == Vector(3, 4))  # True
Vector(4, 6)
Vector(2, 2)
Vector(9, 12)
False
True

Most Useful Dunder Methods for Operator Overloading

Dunder MethodOperatorWhat it enables
__add__(self, other)+Addition between two objects
__sub__(self, other)-Subtraction between two objects
__mul__(self, other)*Multiplication
__eq__(self, other)==Equality check
__lt__(self, other)<Less than comparison
__gt__(self, other)>Greater than comparison
__len__(self)len()Length of the object
__str__(self)print()String representation

9. Abstract Classes — Enforcing Polymorphism

What if you want to guarantee that every child class implements a specific method? Without any enforcement, a developer might forget to add area() to a new Shape class — and the bug only shows up at runtime.

Abstract classes solve this. An abstract class declares a method but intentionally leaves it empty. Any child that inherits from it must implement that method — or Python raises an error the moment you try to create the object.

from abc import ABC, abstractmethod

class Shape(ABC):              # Abstract base class
    @abstractmethod
    def area(self):
        pass                   # No implementation — must be done in child

    @abstractmethod
    def perimeter(self):
        pass

class Circle(Shape):
    def __init__(self, r):
        self.r = r

    def area(self):
        return 3.14159 * self.r ** 2

    def perimeter(self):
        return 2 * 3.14159 * self.r

class Square(Shape):
    def __init__(self, s):
        self.s = s

    def area(self):
        return self.s ** 2

    def perimeter(self):
        return 4 * self.s

shapes = [Circle(5), Square(4)]
for s in shapes:
    print(f"Area: {s.area():.2f} | Perimeter: {s.perimeter():.2f}")
Area: 78.54 | Perimeter: 31.42
Area: 16.00 | Perimeter: 16.00
# Try to create abstract class directly — Python refuses
s = Shape()
TypeError: Can't instantiate abstract class Shape with abstract methods area, perimeter
Abstract classes act as a contract. They say — "I will not let you create me directly. If you want to use me as a parent, you must implement ALL my abstract methods." This prevents incomplete classes from slipping into your codebase.

10. Polymorphism vs Inheritance — Key Differences

FeatureInheritancePolymorphism
Core ideaChild reuses parent's codeSame method name, different behaviour
Requires parent class?Yes — alwaysNot always — duck typing needs no parent
Main keywordclass Child(Parent)Same method name in multiple classes
FocusCode reuse and hierarchyFlexibility and uniform interface
Used forSharing common behaviourTreating different objects the same way
Works without the other?YesYes — duck typing is independent
Inheritance and polymorphism often work together — you use inheritance to create a family of classes, and polymorphism to treat all family members uniformly. But they are separate concepts and can be used independently.

11. Real-Life Project — Payment System

from abc import ABC, abstractmethod

class Payment(ABC):
    """Abstract base for all payment methods."""
    def __init__(self, amount):
        self.amount = amount

    @abstractmethod
    def process(self):
        pass

    @abstractmethod
    def confirm(self):
        pass

    def receipt(self):
        print(f"\n--- Receipt ---")
        print(f"Amount:  ₹{self.amount}")
        self.confirm()


class UPIPayment(Payment):
    def __init__(self, amount, upi_id):
        super().__init__(amount)
        self.upi_id = upi_id

    def process(self):
        print(f"Processing UPI payment to {self.upi_id}...")

    def confirm(self):
        print(f"Method:  UPI → {self.upi_id}")
        print(f"Status:  ✓ Success")


class CardPayment(Payment):
    def __init__(self, amount, card_last4):
        super().__init__(amount)
        self.card_last4 = card_last4

    def process(self):
        print(f"Processing Card payment ending in {self.card_last4}...")

    def confirm(self):
        print(f"Method:  Card ****{self.card_last4}")
        print(f"Status:  ✓ Approved")


class CODPayment(Payment):
    def process(self):
        print(f"Order placed for Cash on Delivery...")

    def confirm(self):
        print(f"Method:  Cash on Delivery")
        print(f"Status:  ✓ Order Confirmed")


# Polymorphism — same interface for all payment types
payments = [
    UPIPayment(1500,  "rohan@upi"),
    CardPayment(3200, "5678"),
    CODPayment(800)
]

for pay in payments:
    pay.process()
    pay.receipt()
Processing UPI payment to rohan@upi...

--- Receipt ---
Amount:  ₹1500
Method:  UPI → rohan@upi
Status:  ✓ Success

Processing Card payment ending in 5678...

--- Receipt ---
Amount:  ₹3200
Method:  Card ****5678
Status:  ✓ Approved

Order placed for Cash on Delivery...

--- Receipt ---
Amount:  ₹800
Method:  Cash on Delivery
Status:  ✓ Order Confirmed

12. Practice Problems

Beginner level

  1. Create Dog, Cat, Cow classes each with a sound() method. Loop through them polymorphically.
  2. Create Circle and Square with area() and perimeter() methods.
  3. Demonstrate len() working differently on string, list, and dict.
  4. Show how + works differently for int, float, string, and list.
  5. Create 3 unrelated classes with a greet() method and call them via duck typing.

Intermediate level

  1. Overload the + operator in a BankAccount class to merge two accounts.
  2. Overload == in a Student class to compare by marks.
  3. Use an abstract class to enforce that all shapes must have area() and perimeter().
  4. Create a media player with Play class. Add Video, Audio, Podcast children — each with a play() method.
  5. Overload __str__ in 3 different classes and observe how print() behaves differently.

Real-life projects

  1. Build a Notification system — Email, SMS, Push — all with a send() method.
  2. Build a Vehicle system — Car, Bike, Truck — each with fuel_cost() method.
  3. Create a Logger — FileLogger, ConsoleLogger, CloudLogger with log() method.
  4. Build Employee types — Permanent, Contract, Intern — each with salary() method.
  5. Create a payment gateway with UPI, Card, Wallet, COD using abstract class.

Think deeper

  1. Is polymorphism possible without inheritance in Python?
  2. What is the difference between duck typing and formal interfaces?
  3. What happens if a child class does not implement an abstract method?
  4. Can you overload the > operator to sort custom objects?
  5. How does Python use polymorphism internally with str(), list(), int()?

13. FAQ

Q1. What is the simplest way to explain polymorphism to a beginner?

Polymorphism means one name, many forms. You call the same method — like area() — on different objects and each one responds based on its own logic. A Circle calculates area one way. A Rectangle does it another way. Same call, different results. Python automatically picks the right version based on which object you called it on.

Q2. Is duck typing the same as polymorphism?

Duck typing is Python's way of achieving polymorphism without requiring formal inheritance. In duck typing, Python only checks whether an object has the method being called — it does not care about the object's class or type. So yes, duck typing is a form of polymorphism, but it is more flexible because it requires no class hierarchy at all.

Q3. What happens if I call a method that a child class did not implement?

Python searches up the MRO — if the parent has the method, Python uses the parent's version. If neither the child nor any parent has it, Python raises an AttributeError. If you use abstract classes, Python raises a TypeError the moment you try to create the object — before you even call the method.

Q4. What is the difference between method overriding and method overloading?

Method overriding happens in inheritance — a child class redefines a method from its parent. Method overloading means having multiple methods with the same name but different parameters — this exists in Java and C++ but Python handles it differently using default arguments and *args instead.

Q5. What is the abc module and why do I need it?

abc stands for Abstract Base Classes. The abc module provides the ABC class and the @abstractmethod decorator. Together they let you define abstract classes — classes that cannot be instantiated directly and that force all child classes to implement specific methods. It is the professional way to enforce a consistent interface across all your subclasses.

Q6. Can I use polymorphism without any OOP at all?

Yes — Python's built-in functions are the best example. len(), print(), and str() work on completely different data types polymorphically. Even the + operator works polymorphically on integers, floats, strings, and lists — all without you defining a single class. Polymorphism is a broader programming concept, not just an OOP feature.


✅ Quick Summary — What You Learned

  • Polymorphism means the same method name behaves differently depending on which object calls it
  • Python's built-in operators and functions like +, len() are already polymorphic
  • Method overriding in inherited classes is the most common form of polymorphism
  • Duck typing — Python checks if the method exists, not what type the object is
  • Operator overloading uses dunder methods like __add__, __eq__, __str__
  • Abstract classes (using abc module) enforce that child classes implement required methods
  • You cannot create an abstract class directly — it exists only to be inherited
  • Polymorphism makes loops that work on mixed object types clean and scalable

"Polymorphism is Python's way of saying — I don't care who you are. If you can do the job, you're hired."

— Code with Py

Found this post useful? Share it with a classmate learning OOP. Try the payment project — it ties everything together perfectly!

Comments

Popular posts from this blog

Python Strings – A Complete Guide for Beginners (with Examples)

Python Control Flow Statements – Complete Guide with Real-Life Examples

Python Loops Explained – for Loop and while Loop with Examples and Output