Polymorphism & Duck Typing

intermediate polymorphism duck-typing EAFP operator-overloading singledispatch

Polymorphism means “many forms.” In simple language, it means the same operation behaves differently depending on the object we’re working with. We call len() on a list, a string, or a dictionary — same function, different behavior. That’s polymorphism.

Method Overriding

The most basic form. A child class redefines a method from the parent, and Python uses the child’s version.

class Shape:
    def area(self):
        return 0

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

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

class Rectangle(Shape):
    def __init__(self, w, h):
        self.w, self.h = w, h

    def area(self):  # overrides Shape.area
        return self.w * self.h

Now we can write code that works with any shape:

shapes = [Circle(5), Rectangle(4, 6)]
for s in shapes:
    print(s.area())  # each shape calculates its own area
# 78.53975
# 24

We don’t care what type s is. We just call .area() and each object handles it.

Duck Typing

“If it walks like a duck and quacks like a duck, then it must be a duck.”

Python doesn’t care about an object’s type. It cares about what the object can do. If an object has the method we’re calling, Python is happy — no inheritance required.

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

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

class Robot:
    def speak(self):
        return "Beep boop!"

# No shared parent class needed!
for thing in [Dog(), Cat(), Robot()]:
    print(thing.speak())

Dog, Cat, and Robot have zero relationship. But they all have .speak(), and that’s enough. This is duck typing in action.

EAFP vs LBYL

Python’s duck typing philosophy extends to error handling with two competing styles:

LBYL (Look Before You Leap) — check first, then act:

if hasattr(obj, "speak"):  # check first
    obj.speak()

EAFP (Easier to Ask Forgiveness than Permission) — just do it, handle errors if they happen:

try:
    obj.speak()  # just go for it
except AttributeError:
    print("This object can't speak")

EAFP is the Pythonic way. It’s usually faster too, because in the happy path we skip the check entirely.

Operator Overloading

Python lets us define what operators like +, *, ==, and < do for our objects. We do this by implementing special dunder methods.

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):       # v1 + v2
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):      # v * 3
        return Vector(self.x * scalar, self.y * scalar)

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

v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)    # Vector(4, 6)
print(v1 * 3)     # Vector(3, 6)

Common operator dunder methods: __add__ (+), __sub__ (-), __mul__ (*), __eq__ (==), __lt__ (<), __len__ (len()), __getitem__ ([]).

Python Has No Method Overloading

In languages like Java, we can define multiple methods with the same name but different parameter types. Python doesn’t support this — the last definition wins.

class Calculator:
    def add(self, a, b):
        return a + b

    def add(self, a, b, c):  # this REPLACES the previous add!
        return a + b + c

calc = Calculator()
# calc.add(1, 2)     # TypeError — the two-arg version is gone
print(calc.add(1, 2, 3))  # 6

Instead, we use default arguments or *args:

class Calculator:
    def add(self, *args):  # accept any number of arguments
        return sum(args)

calc = Calculator()
print(calc.add(1, 2))      # 3
print(calc.add(1, 2, 3))   # 6

functools.singledispatch for Type-Based Dispatch

If we genuinely need different behavior based on argument type, Python offers singledispatch. It works with regular functions (not methods).

from functools import singledispatch

@singledispatch
def process(data):
    raise TypeError(f"Unsupported type: {type(data)}")

@process.register(str)
def _(data):
    return data.upper()

@process.register(list)
def _(data):
    return [x * 2 for x in data]

print(process("hello"))    # HELLO
print(process([1, 2, 3]))  # [2, 4, 6]

For methods inside a class, Python 3.8+ has functools.singledispatchmethod.

In simple language, polymorphism means “same interface, different behavior.” Python achieves it through method overriding, duck typing, and operator overloading — and it prefers duck typing over rigid type checking, because what matters is what an object can do, not what it is.