---
title: Pillar Four: Polymorphism in Python
source: https://app.sythra.ai/learn/python/polymorphism-python
topic: Python
updated: 2026-08-12
publisher: Sythra (https://app.sythra.ai)
---

# Pillar Four: Polymorphism in Python

Polymorphism means the same method name or operator can behave differently depending on the object it's used with. Python achieves this through duck typing (any object with the right method works, regardless of type) and dunder methods like __add__ and __eq__, which let your own classes plug into built-in operators and functions.

_Source: [https://app.sythra.ai/learn/python/polymorphism-python](https://app.sythra.ai/learn/python/polymorphism-python) — free to read on Sythra._

## Key points

- Duck typing: Python calls whatever method exists on an object, without requiring a shared parent class
- len(), print(), and sorted() are polymorphic because they call dunder methods like __len__ and __str__ that each type defines its own way
- Defining __add__ (and __radd__) lets the + operator work on your own custom classes
- Defining __eq__ teaches Python what equality actually means for your class, instead of checking identity by default
- Polymorphism paired with abstract classes lets one function correctly handle many different object types with zero special-casing

Back in [The Four Pillars of OOP](/learn/python/four-pillars-of-oop)'s coffee machine story, polymorphism was the idea that pressing "Brew" on a drip machine and pressing "Brew" on an espresso machine triggers two completely different sequences of actions, even though the button says the exact same word. The _name_ stays the same; the _behavior_ changes depending on which machine you're using.

That's the whole idea behind this final pillar: **the same name, used on different types of objects, can do different things — and that's a deliberate, powerful feature, not a bug.** This article covers several flavors: polymorphism through inheritance, "duck typing," and operator overloading you build yourself.

## What you will learn

- Polymorphism you already get for free through inheritance
- "Duck typing" — Python's willingness to call any method that exists, regardless of type
- How built-ins like `len()` use polymorphism via `__len__`
- Operator overloading — teaching `+`, `==`, and more to understand your own classes
- A final example combining all four pillars in one small program

## Polymorphism you already have, through inheritance

Remember `move_cards` from [Inheritance](/learn/python/inheritance-python)? It called `hand.add_card()` without checking whether `hand` was actually a `Hand` or a plain `Deck` — it just trusted the method would be there. That trust is polymorphism: the same code working across related types without caring which one it's holding, safe because `Hand` and `Deck` share a common ancestor.

## Duck typing: "if it walks like a duck..."

Python doesn't actually care what _type_ an object officially is — only whether it can actually _do_ the thing you're asking, right now. This idea is nicknamed **duck typing**: "if it walks like a duck and quacks like a duck, it's a duck." In Python terms: "if it has the method I'm trying to call, I don't care what class it officially belongs to."

```python
class Duck:
    def make_sound(self):
        return "Quack!"

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

class Car:
    def make_sound(self):
        return "Vroom!"

def make_it_speak(thing):
    print(thing.make_sound())

make_it_speak(Duck())   # Quack!
make_it_speak(Dog())    # Woof!
make_it_speak(Car())    # Vroom!
```

`Duck`, `Dog`, and `Car` are **completely unrelated classes** — none inherit from each other, none share a common parent. Yet `make_it_speak` works perfectly with all three, because it never cares what _type_ `thing` is — only that it has a `make_sound()` method. This is polymorphism completely unconnected to inheritance — pure duck typing, a meaningfully different approach from languages that insist on shared ancestors or declared interfaces.

## The built-in functions that already use polymorphism

```python
len("hello")          # 5    (works on strings)
len([1, 2, 3])         # 3    (works on lists)
len({'a': 1, 'b': 2})  # 2    (works on dictionaries)
len({1, 2, 3})         # 3    (works on sets)
```

`len()` is a single function that correctly handles completely different types. How? Under the hood, every one of these types implements a special method called `__len__`, and `len()` simply calls `.__len__()` on whatever you hand it. Each type defines `__len__` _its own way_, but `len()` itself doesn't need to know the details — it just trusts the method exists. `sorted()`, `print()`, `str()`, and `in` all work this same general way.

## Operator overloading: teaching + to understand your own classes

So far, `+` only knows how to work with things Python already understands. What if you wanted `+` to work on a `Time`, combining it with a duration in seconds?

```python
def int_to_time(seconds):
    minutes, second = divmod(seconds, 60)
    hour, minute = divmod(minutes, 60)
    return Time(hour, minute, second)

# inside class Time:
def __add__(self, other):
    seconds = self.time_to_int() + other
    return int_to_time(seconds)
```

The moment `Time` has an `__add__` method, `+` quietly starts working on it:

```python
start = Time(9, 45, 0)
duration = 1500   # seconds
print(start + duration)
# → 10:10:00
```

Python sees `start + duration`, notices `start` is a `Time`, and silently translates that into `start.__add__(duration)`. This translation is called **operator overloading** — a specific, powerful flavor of polymorphism: the exact same `+` symbol means something different depending on the types on either side. As covered in [Classes and Methods](/learn/python/classes-and-methods-python), you can combine this with `isinstance` to handle both `Time + Time` and `Time + int`, plus `__radd__` to catch the reversed case `1500 + start`. Once `__add__` is implemented, `sum()` works on a list of `Time` objects for free.

## Other useful special methods

`__add__` is just one of a family of special methods (sometimes nicknamed "**dunder methods**," short for "double underscore"), each hooking into a different built-in behavior:

```python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        """Called by print() and str()"""
        return f'({self.x}, {self.y})'

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

    def __add__(self, other):
        """Called by +"""
        return Point(self.x + other.x, self.y + other.y)

    def __len__(self):
        """Called by len() — here, distance from origin, rounded down"""
        return int((self.x ** 2 + self.y ** 2) ** 0.5)
```

```python
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

print(p1)            # (1, 2)   uses __str__
print(p1 == p2)      # True     uses __eq__
print(p1 == p3)      # False    uses __eq__
print(p1 + p3)       # (4, 6)   uses __add__
print(len(p3))       # 5        uses __len__
```

This connects directly to something from [Classes and Objects](/learn/python/classes-and-objects-python): without `__eq__`, Python's default `==` checks identity, not content — two separate `Point` objects with identical coordinates would be "not equal" simply because they're different objects in memory. Defining `__eq__` teaches Python what "equal" actually means for your class.

## Polymorphism working together with inheritance

Polymorphism becomes especially elegant combined with inheritance and abstract classes, like `Shape`, `Circle`, and `Rectangle` from [Abstraction](/learn/python/abstraction-python):

```python
shapes = [Circle(5), Rectangle(4, 6)]

for shape in shapes:
    print(f"Area: {shape.area()}, Perimeter: {shape.perimeter()}")

# Area: 78.53975, Perimeter: 31.4159
# Area: 24, Perimeter: 20
```

This loop calls `.area()` and `.perimeter()` on every shape — but `Circle.area()` and `Rectangle.area()` compute completely different formulas underneath. The loop never needed an `if isinstance(...): ... elif isinstance(...): ...` chain. It simply trusts that _whatever_ `shape` is, calling `.area()` does the right thing — polymorphism delivering its full promise: **one piece of code, correctly handling many types, with zero special-casing.**

## Putting it all together: all four pillars, in one place

```python
from abc import ABC, abstractmethod

# ABSTRACTION — a contract that all animals must follow
class Animal(ABC):
    def __init__(self, name):
        self._name = name   # ENCAPSULATION — protected, accessed via property

    @property
    def name(self):
        return self._name

    @abstractmethod
    def make_sound(self):
        """Every animal must define its own sound."""
        pass

    def __str__(self):
        return f"{self.name} says {self.make_sound()}"


# INHERITANCE — Dog and Cat build on top of Animal
class Dog(Animal):
    def make_sound(self):
        return "Woof!"

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


# POLYMORPHISM — the same loop, same method calls, different behavior each time
animals = [Dog("Rex"), Cat("Whiskers")]
for animal in animals:
    print(animal)

# Rex says Woof!
# Whiskers says Meow!
```

- **Encapsulation** — `_name` is protected, only reachable through the `name` property.
- **Abstraction** — `Animal` defines a contract (`make_sound` must exist) without implementing it, and `Animal()` can never be created directly.
- **Inheritance** — `Dog` and `Cat` both build on `Animal`, inheriting `__str__` and the `name` property unchanged.
- **Polymorphism** — the same loop, calling the same `print(animal)`, produces different output for each object, because `make_sound()` means something different depending on which animal it's called on.

Four separate ideas, each solving its own problem, all working together inside one small, clean piece of code.

## Common mistakes

- Writing an `if isinstance(...): ... elif isinstance(...): ...` chain instead of trusting a shared interface — this is exactly what polymorphism is meant to eliminate
- Forgetting `__radd__`, so `int + obj` fails even though `obj + int` works
- Defining `__eq__` but expecting `hash()` or set membership to keep working automatically — they often need `__hash__` too
- Assuming duck typing requires any inheritance relationship at all — it explicitly does not

> **Practice with Sythra:**  [Practice with AI tutor](https://app.sythra.ai/pricing)

## FAQ

### What is polymorphism in Python?

Polymorphism means the same method name, function, or operator can behave differently depending on the type of object it's used with. In Python this is enabled both through inheritance and through duck typing.

### What is duck typing in Python?

Duck typing means Python doesn't check an object's type before calling a method — it just tries to call the method, and if the object has it, the call succeeds. This works even for completely unrelated classes with no shared parent.

### How does len() work on different types like strings and lists?

len() calls the __len__ method on whatever object you give it. Each type — string, list, dict, set — implements __len__ its own way, but len() itself doesn't need to know those details; it just trusts the method exists.

### How do you make the + operator work on your own class in Python?

Define an __add__ method on the class. When Python evaluates a + b, it calls a.__add__(b). You can also define __radd__ to handle the reversed case, like when a plain number appears on the left side of the +.

### Why does == return False for two objects with the same values by default?

Without a custom __eq__ method, Python's default == checks whether two objects are literally the same object in memory (identity), not whether their attribute values match. Defining __eq__ lets you specify what equality actually means for your class.

## Related

- [The Four Pillars of OOP](https://app.sythra.ai/learn/python/four-pillars-of-oop) — The overview these four deep-dive articles build from.
- [Pillar Three: Abstraction](https://app.sythra.ai/learn/python/abstraction-python) — The shared contract that makes polymorphism reliable.
- [Classes and Methods in Python](https://app.sythra.ai/learn/python/classes-and-methods-python) — self, __init__, and the basics of operator overloading.
- [Python course hub](https://app.sythra.ai/learn/python) — All free Python explainers and the path into Agentic practice.

---

Written by Sythra — Learn machine learning by building. Practice this topic with Sythra's AI tutor: https://app.sythra.ai/pricing
