Python Lesson 33 – Polymorphism | Dataplexa

Polymorphism

The word polymorphism comes from Greek — it means "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 got a first glimpse of this in the Inheritance lesson when different animal subclasses each had their own speak() method. This lesson goes much deeper — covering every form of polymorphism Python supports and showing you how it makes code dramatically more flexible and extensible.

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.

What Polymorphism Means in Practice

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.

# The core idea — same call, different behaviour per type

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 needed
def make_noise(animal):
    print(animal.speak())

make_noise(Dog())    # Woof!
make_noise(Cat())    # Meow!
make_noise(Duck())   # Quack!
Woof!
Meow!
Quack!
  • make_noise() does not care about the type — it only cares that the object has a speak() method
  • Adding a new animal type requires no changes to make_noise() — just define the method on the new class
  • This is the open/closed principle — open for extension, closed for modification

1. Polymorphism Through Inheritance

The most explicit form of polymorphism in Python is overriding a parent method in each subclass. A shared interface is defined in the parent and each child implements it differently.

Real-world use: a reporting system defines a base Report class with a generate() method. Each subclass — PDFReport, CSVReport, HTMLReport — overrides generate() to produce output in its own format. The code that calls generate() never needs to know which format it is dealing with.

# Polymorphism through inheritance — shared interface, different implementations

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

    def describe(self):
        print(f"I am a {self.__class__.__name__} with area {self.area():.2f}")

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

    def area(self):
        import math
        return math.pi * self.radius ** 2

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

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

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

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

# One loop — works for every shape type
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]

for shape in shapes:
    shape.describe()   # calls the right area() for each type
I am a Circle with area 78.54
I am a Rectangle with area 24.00
I am a Triangle with area 12.00
  • describe() is defined once in the parent and calls self.area() — Python dispatches to the correct subclass version at runtime
  • Raising NotImplementedError in the parent enforces that every subclass must provide its own area()
  • Adding a Pentagon class requires only writing the new class — nothing else changes

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 the object's type. This is called duck typing: "if it walks like a duck and quacks like a duck, it is a duck."

Why it exists: Python is dynamically typed. Rather than checking types at compile time, Python checks at runtime whether the object supports the operation. This makes code more flexible and reusable.

Real-world use: Python's built-in len() works on strings, lists, tuples, dicts, sets, and any custom class that implements __len__ — regardless of their inheritance hierarchy.

# Duck typing — no shared parent required

class PDFExporter:
    def export(self, data):
        print(f"Exporting to PDF: {data}")

class CSVExporter:
    def export(self, data):
        print(f"Exporting to CSV: {data}")

class JSONExporter:
    def export(self, data):
        import json
        print(f"Exporting to JSON: {json.dumps(data)}")

# This function works with any object that has an export() method
def run_export(exporter, data):
    exporter.export(data)   # duck typing — no isinstance() check

records = {"sales": 1200, "region": "US"}

run_export(PDFExporter(),  records)
run_export(CSVExporter(),  records)
run_export(JSONExporter(), records)
Exporting to PDF: {'sales': 1200, 'region': 'US'}
Exporting to CSV: {'sales': 1200, 'region': 'US'}
Exporting to JSON: {"sales": 1200, "region": "US"}
  • 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 — Same Operator, Different Types

Python's built-in operators are polymorphic by design. The + operator adds numbers, concatenates strings, and merges lists — the behaviour depends entirely on the type of the operands.

# Built-in operator polymorphism

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

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

# * is polymorphic too
print("ha" * 3)          # hahaha
print([0] * 4)           # [0, 0, 0, 0]
15
15.0
Hello World
[1, 2, 3, 4]
6
3
1
hahaha
[0, 0, 0, 0]
  • This works because each type implements the underlying magic methods (__add__, __len__, __mul__) in its own way
  • You extend this to your own classes using magic methods — covered in the next lesson

4. Function and Method Polymorphism

Python's built-in functions like str(), repr(), iter(), and sorted() are all polymorphic — they work on any object that implements the corresponding interface. You can make your own functions equally flexible.

# Polymorphic functions — work with any compatible object

class Celsius:
    def __init__(self, temp):
        self.temp = temp
    def __str__(self):
        return f"{self.temp}°C"
    def __lt__(self, other):
        return self.temp < other.temp

class Fahrenheit:
    def __init__(self, temp):
        self.temp = temp
    def __str__(self):
        return f"{self.temp}°F"
    def __lt__(self, other):
        return self.temp < other.temp

temps = [Celsius(100), Celsius(0), Celsius(37), Fahrenheit(98), Fahrenheit(32)]

# str() is polymorphic — calls __str__ on each type
for t in temps:
    print(str(t), end="  ")
print()

# sorted() is polymorphic — calls __lt__ on each type
celsius_only = [Celsius(100), Celsius(0), Celsius(37)]
print(sorted(celsius_only))
100°C 0°C 37°C 98°F 32°F
[0°C, 37°C, 100°C]

5. Polymorphism with isinstance() — When Type Checking is Right

Pure duck typing is ideal, but occasionally you need to handle different types differently in the same function. In those cases, isinstance() is the right tool — not type() ==. Using isinstance() respects inheritance.

# isinstance() — type-aware polymorphism when needed

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

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

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

class GoldenRetriever(Dog):   # subclass of Dog
    pass

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

check_type(Dog("Rex"))
check_type(Cat("Luna"))
check_type(GoldenRetriever("Buddy"))   # correctly identified as Dog

# Contrast with type() == which does NOT respect inheritance
print(type(GoldenRetriever("Buddy")) == Dog)   # False — too strict
Rex is a Dog (or subclass)
Luna is a Cat
Buddy is a Dog (or subclass)
False
  • isinstance(obj, Class) returns True for the class and all its subclasses
  • type(obj) == Class returns True only for the exact class — misses subclasses
  • Prefer duck typing over isinstance() checks — only use isinstance() when you genuinely need to branch based on type

Summary Table

Form How It Works Key Idea
Inheritance-based Subclasses override a shared parent method Runtime dispatch to correct subclass version
Duck typing Any object with the required method works No shared parent needed — just the right interface
Operator polymorphism Same operator behaves differently per type Powered by magic methods
isinstance() Type-aware branching that respects inheritance Use sparingly — prefer duck typing

Practice Questions

Practice 1. What does polymorphism mean in the context of Python?



Practice 2. What is duck typing?



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



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



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



Quiz

Quiz 1. Which of the following best describes polymorphism?






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






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






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






Quiz 5. Which of the following demonstrates duck typing correctly?






Next up — Encapsulation: controlling access to data with private attributes, getters, setters, and the @property decorator.