Chapters
Python114 chapters

Classes and objectsChapter 65 of 114

Polymorphism

Different types answering the same call, and why Python barely notices.

The same call, different objects

Polymorphism means code that works with any object providing the operation it needs:

Python
class Dog:
    def speak(self):
        return "woof"

class Cat:
    def speak(self):
        return "meow"

class Robot:
    def speak(self):
        return "beep"

for thing in [Dog(), Cat(), Robot()]:
    print(thing.speak())

Output

woof
meow
beep

Note Robot is not related to the others at all. It just has speak, and that is enough.

Duck typing

The rule is: if it walks like a duck and quacks like a duck, treat it as a duck. Python checks whether the operation works, not what the type is:

Python
def describe(items):
    return f"{len(items)} items, first is {items[0]}"

print(describe([1, 2, 3]))
print(describe("abc"))
print(describe((True, False)))

Output

3 items, first is 1
3 items, first is a
2 items, first is True

One function, three unrelated types, no inheritance anywhere.

Builtins are polymorphic too

len(), +, in and for all work on anything that implements the right method, including yours:

Python
class Playlist:
    def __init__(self, songs):
        self.songs = songs

    def __len__(self):
        return len(self.songs)

    def __getitem__(self, index):
        return self.songs[index]

p = Playlist(["a", "b", "c"])
print(len(p))
print(p[1])
print([song for song in p])

Output

3
b
['a', 'b', 'c']

Defining __getitem__ was enough to make the object loopable. That is polymorphism in the other direction: your class fitting into Python's rules.

Checking capability, not type

Asking whether something can do the job is more flexible than asking what it is:

Python
def make_speak(thing):
    if hasattr(thing, "speak"):
        return thing.speak()
    return "cannot speak"

class Dog:
    def speak(self):
        return "woof"

print(make_speak(Dog()))
print(make_speak(42))

Output

woof
cannot speak

Often the cleanest version does neither, and simply tries:

Python
def make_speak(thing):
    try:
        return thing.speak()
    except AttributeError:
        return "cannot speak"

print(make_speak(42))

Output

cannot speak

Abstract base classes

When you do want to insist on an interface, abc refuses to build a subclass that has not implemented it:

Python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        ...

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

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

print(Square(3).area())

class Blob(Shape):
    pass

try:
    Blob()
except TypeError as problem:
    print("TypeError:", problem)

Output

9
TypeError: Can't instantiate abstract class Blob without an implementation for abstract method 'area'

The error arrives when someone tries to build the incomplete class, rather than much later when something calls the missing method.

Operator overloading

Define the dunder and the operator works:

Python
class Money:
    def __init__(self, amount):
        self.amount = amount

    def __add__(self, other):
        return Money(self.amount + other.amount)

    def __str__(self):
        return f"{self.amount:.2f}"

print(Money(4.5) + Money(2.25))

Output

6.75

Use it where the meaning is obvious. + on two amounts of money reads well; + meaning "send an email" does not.

Test yourself

2 questions

What is duck typing?

Show the answer

Caring whether an object supports the operation, not what type it is — It is why a function can accept a list, a string and a tuple with no shared base class.

What does an abstract base class give you?

Show the answer

An error when someone builds a subclass that has not implemented a required method — The error arrives at construction rather than much later, when something calls the missing method.

Next chapter

Iterators

How for loops actually work, and how to make your own object loopable.