Polymorphism in Python | Python Course | Dataplexa

Polymorphism in Python

The word polymorphism comes from Greek — "many forms." In Python, it describes the ability of different object types to respond to the same method call, each in their own way. You saw this in the Inheritance lesson when different animal subclasses had their own speak() method. This lesson goes much deeper — covering every form of polymorphism Python supports, including duck typing, operator overloading, and the design principles that make polymorphism so valuable.

Polymorphism is not a feature you bolt on — it is a natural consequence of good object design. Once you understand it, you will start seeing and using it everywhere.

The Core Idea

In Python, polymorphism means you can write code that works on objects of different types — as long as those objects support the operations you are calling. You do not need to check what type something is. You just call the method and trust each object to do the right thing.

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

class Cat:
    def speak(self): return "Meow!"

class Duck:
    def speak(self): return "Quack!"

# A single function works with any of these — no isinstance() checks
def make_noise(animal):
    print(animal.speak())

for creature in [Dog(), Cat(), Duck(), Dog()]:
    make_noise(creature)

# Adding a new type requires zero changes to make_noise()
class Parrot:
    def speak(self): return "Polly wants a cracker!"

make_noise(Parrot())
Woof! Meow! Quack! Woof! Polly wants a cracker!
  • make_noise() does not care about the type — it only cares that the object has a speak() method.
  • Adding a new type requires no changes to any existing code — this is the open/closed principle: open for extension, closed for modification.

1. Polymorphism Through Inheritance

The most explicit form: override a parent method in each subclass. A shared interface is defined once in the parent and each child implements it differently.

import math

class Shape:
    def area(self):
        raise NotImplementedError("Subclasses must implement area()")

    def perimeter(self):
        raise NotImplementedError("Subclasses must implement perimeter()")

    def describe(self):
        print(f"{self.__class__.__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}")

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    def area(self):
        return math.pi * self.radius ** 2
    def perimeter(self):
        return 2 * math.pi * self.radius

class Rectangle(Shape):
    def __init__(self, w, h):
        self.w, self.h = w, h
    def area(self):
        return self.w * self.h
    def perimeter(self):
        return 2 * (self.w + self.h)

class Triangle(Shape):
    def __init__(self, a, b, c):
        self.a, self.b, self.c = a, b, c
    def area(self):
        s = (self.a + self.b + self.c) / 2
        return math.sqrt(s * (s-self.a) * (s-self.b) * (s-self.c))
    def perimeter(self):
        return self.a + self.b + self.c

shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 4, 5)]
for s in shapes:
    s.describe()

total_area = sum(s.area() for s in shapes)
print(f"\nTotal area: {total_area:.2f}")
Circle: area=78.54, perimeter=31.42 Rectangle: area=24.00, perimeter=20.00 Triangle: area=6.00, perimeter=12.00 Total area: 108.54
  • describe() is defined once and calls self.area() and self.perimeter() — Python dispatches to the correct subclass version at runtime.
  • Adding a Pentagon class requires only writing the new class — the loop, sum(), and describe() all work unchanged.

2. Duck Typing — Python's Natural Polymorphism

Python does not require a shared parent class for polymorphism. If an object has the method you need, you can call it — regardless of type. This is duck typing: "if it walks like a duck and quacks like a duck, it is a duck."

import json

# No shared parent — just the same interface (export method)
class PDFExporter:
    def export(self, data):
        print(f"[PDF] Writing {len(data)} records to report.pdf")

class CSVExporter:
    def export(self, data):
        headers = ",".join(data[0].keys())
        rows    = len(data)
        print(f"[CSV] {headers} — {rows} rows written to report.csv")

class JSONExporter:
    def export(self, data):
        print(f"[JSON] {json.dumps(data[:1])}... ({len(data)} records)")

# This function works with any exporter — duck typing
def run_export(exporter, data):
    exporter.export(data)

records = [
    {"name": "Alice", "score": 92},
    {"name": "Bob",   "score": 88},
    {"name": "Carol", "score": 95},
]

for exporter in [PDFExporter(), CSVExporter(), JSONExporter()]:
    run_export(exporter, records)
[PDF] Writing 3 records to report.pdf [CSV] name,score — 3 rows written to report.csv [JSON] [{"name": "Alice", "score": 92}]... (3 records)
  • None of the exporter classes share a parent — duck typing does not require it.
  • If an object does not have the required method, Python raises AttributeError at call time — not before.
  • Duck typing is why Python code is often more concise and composable than equivalent code in statically typed languages.

3. Operator Polymorphism — Built-In Operators

Python's operators are polymorphic by design — the same symbol does different things depending on the type. This works because each type implements the underlying dunder method in its own way.

# Same operators — different types, different behaviour

print(10 + 5)               # 15          — integer addition
print(10.5 + 5)             # 15.5        — float addition
print("Hello" + " World")   # Hello World — string concatenation
print([1, 2] + [3, 4])      # [1, 2, 3, 4] — list merge

print(len("Python"))        # 6  — string length via __len__
print(len([1, 2, 3]))       # 3  — list length via __len__
print(len({"a": 1, "b": 2}))  # 2  — dict length via __len__

print("ha" * 3)             # hahaha     — string repeat
print([0] * 5)              # [0,0,0,0,0] — list repeat

# Your own class can join this via dunder methods
class Money:
    def __init__(self, amount, currency="USD"):
        self.amount   = amount
        self.currency = currency
    def __add__(self, other):
        if self.currency != other.currency:
            raise ValueError("Currency mismatch")
        return Money(self.amount + other.amount, self.currency)
    def __repr__(self):
        return f"Money({self.amount}, {self.currency!r})"

a = Money(10); b = Money(25)
print(a + b)   # Money(35, 'USD') — same + operator, custom type
15 15.5 Hello World [1, 2, 3, 4] 6 3 2 hahaha [0, 0, 0, 0, 0] Money(35, 'USD')

4. Polymorphism with Built-In Functions

Python's built-in functions like str(), len(), iter(), sorted(), and repr() are all polymorphic — they work on any object that implements the corresponding dunder method.

class Product:
    def __init__(self, name, price, stock):
        self.name  = name
        self.price = price
        self.stock = stock

    def __str__(self):   return f"{self.name} — ${self.price:.2f}"
    def __repr__(self):  return f"Product({self.name!r}, {self.price})"
    def __len__(self):   return self.stock
    def __bool__(self):  return self.stock > 0
    def __lt__(self, other): return self.price < other.price

items = [
    Product("Desk",     89.99, 5),
    Product("Notebook",  4.99, 0),
    Product("Pen",       1.50, 50),
    Product("Lamp",     24.99, 12),
]

# str() — polymorphic via __str__
for p in items:
    print(str(p))

# bool() — polymorphic via __bool__
in_stock = [p for p in items if p]   # uses __bool__
print(f"\nIn stock ({len(in_stock)}):", [p.name for p in in_stock])

# sorted() — polymorphic via __lt__
by_price = sorted(items)
print("By price:", [p.name for p in by_price])
Desk — $89.99 Notebook — $4.99 Pen — $1.50 Lamp — $24.99 In stock (3): ['Desk', 'Pen', 'Lamp'] By price: ['Pen', 'Notebook', 'Lamp', 'Desk']

5. isinstance() vs type() — When Type Checking is Needed

Pure duck typing is ideal, but occasionally you need to handle different types differently. In those cases, always prefer isinstance() over type() ==.

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

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

class GuideDog(Dog):   # subclass of Dog
    pass

class Cat(Animal):
    def speak(self): return "Meow!"

def check_type(animal):
    if isinstance(animal, Dog):    # True for Dog AND GuideDog
        print(f"{animal.name}: is a Dog or subclass")
    elif isinstance(animal, Cat):
        print(f"{animal.name}: is a Cat")

check_type(Dog("Rex"))
check_type(GuideDog("Max"))     # correctly identified as Dog
check_type(Cat("Luna"))

# isinstance() vs type() — critical difference
buddy = GuideDog("Buddy")
print(isinstance(buddy, Dog))      # True  — respects inheritance
print(type(buddy) == Dog)          # False — too strict, misses subclass
Rex: is a Dog or subclass Max: is a Dog or subclass Luna: is a Cat True False
  • isinstance(obj, Class) returns True for the class and all its subclasses — correct for polymorphic code.
  • type(obj) == Class returns True only for the exact class — misses subclasses and breaks polymorphism.
  • Prefer duck typing over isinstance() checks — only use isinstance() when you genuinely need to branch based on type.

Quick Reference Table

FormHow It WorksKey Idea
Inheritance-basedSubclasses override a shared parent methodRuntime dispatch to correct subclass version
Duck typingAny object with the required method worksNo shared parent needed — just the right interface
Operator overloadingSame operator behaves differently per typePowered by dunder methods like __add__, __len__
Built-in functionsstr(), len(), sorted() work on any compatible typeEach type implements __str__, __len__, __lt__ etc.
isinstance()Type-aware branching that respects inheritanceUse sparingly — prefer duck typing

Practice

What does polymorphism mean in Python?



What is duck typing?



Why is isinstance() preferred over type() == when type checking is needed?



What design principle does polymorphism support — open for extension, closed for modification?



What happens if you call a method on an object that does not have it in duck-typed code?



Which dunder method enables sorted() to work on your custom objects?



Quick Quiz

Which best describes polymorphism?





In duck typing, what is the only requirement for an object to be used in a function?





Why does len() work equally on strings, lists, dicts, and custom classes?





When a parent class raises NotImplementedError in a method, what is it communicating?





Which of the following demonstrates duck typing correctly?





Does isinstance(GuideDog("Max"), Dog) return True when GuideDog is a subclass of Dog?





NEXT UP
Encapsulation in Python
Controlling access to data with private attributes, getters, setters, and the @property decorator — the professional way to protect object state.