Python Polymorphism
Object-Oriented Python Object-Oriented Python

Python Polymorphism and Duck Typing

1Python Polymorphism: overview

Polymorphism means different kinds of objects respond to the same method call in their own way, and in Python duck typing means an object only needs to have the right method, not the right parent class, to be used.

GoalBy the end you can write one function or loop that works with many different objects, use overriding and duck typing to get different behaviour from the same call, spot polymorphism in built-ins like len() and +, avoid long isinstance() chains, and check for a method with hasattr() or try/except.

Before this lesson: Python Inheritance

2Python Polymorphism: diagram

for method in wallet: method.pay(50)for method in wallet: method.pay(50)objectclassinherits frommethod.pay(50) giveswallet[0]PaymentMethod-50.00 paidwallet[1]CreditCardPaymentMethod51.50 charged to cardwallet[2]GiftVoucher-30.00 taken from voucherOne identical call, three different results, and wallet[2] is not related to the other two
The loop makes the same call on every object. The result depends on the object's own class. GiftVoucher has no parent at all, yet it is accepted because it has a pay() method, which is duck typing.

3Python Polymorphism: explained step by step

Polymorphism (Greek for "many forms") means one name, many behaviours. You call thing.send(message) or len(thing) without knowing exactly what thing is, and each kind of object does the right thing in its own way. The code that makes the call stays short and never has to ask "what are you?".

You met a first taste of this in the inheritance lesson. Here you will see the three places where polymorphism shows up in Python: overridden methods in a class family, duck typing with unrelated classes, and the built-in functions and operators themselves.

One call, many behaviours: overriding

When a parent class and its children all define a method with the same name, a call runs the version that belongs to the object it was made on. A child can replace the parent's version completely, or build on it with super():

Python
class Sender:
    def __init__(self, recipient):
        self.recipient = recipient

    def send(self, message):
        return f"To {self.recipient}: {message}"

class EmailSender(Sender):
    def send(self, message):
        return "Email " + super().send(message)

class SmsSender(Sender):
    def send(self, message):
        return "SMS " + super().send(message[:19] + "...")    # texts must be short

class PushSender(Sender):
    def send(self, message):
        return f"Push banner for {self.recipient}: {message.upper()}"

channels = [EmailSender("asha@example.com"), SmsSender("+1-555-0100"), PushSender("Asha's phone")]
for channel in channels:
    print(channel.send("Your parcel arrives tomorrow"))

The list holds three different kinds of sender, yet the line channel.send(...) is identical for all of them. Python looks in each object's own class first, so each object gets its own behaviour. Add a fourth channel next month and the loop does not change at all.

Duck typing: "if it quacks like a duck..."

Python never checks an object's class before calling a method. At the moment of the call it only asks whether the object has that method. The saying goes: if it walks like a duck and quacks like a duck, it is a duck. So classes do not need a shared parent to be interchangeable. They just need the same method name and compatible arguments.

Python
import json

class CsvExporter:
    def export(self, rows):
        return "\n".join(",".join(str(cell) for cell in row) for row in rows)

class JsonExporter:
    def export(self, rows):
        return json.dumps(rows)

class HtmlExporter:
    def export(self, rows):
        lines = []
        for row in rows:
            cells = "".join(f"<td>{cell}</td>" for cell in row)
            lines.append(f"<tr>{cells}</tr>")
        return "\n".join(lines)

def save_report(exporter, rows):
    print(exporter.export(rows))

rows = [("Asha", 91), ("Ben", 78)]
for exporter in [CsvExporter(), JsonExporter(), HtmlExporter()]:
    save_report(exporter, rows)

None of the three exporters inherits from another, and save_report has no idea which one it received. Hand it anything with an export(rows) method — a class you write next year, a stand-in used in tests, a class from someone else's library — and it works.

Built-in functions and operators are polymorphic too

You have been using polymorphism since your first lessons:

Python
things = ["python", [1, 2, 3], {"a": 1, "b": 2}, (7, 8)]
for thing in things:
    print(type(thing).__name__, len(thing))

print(3 + 4)
print("3" + "4")
print([3] + [4])
print(3 * 4, "3" * 4, [3] * 4)

len() works on strings, lists, dictionaries and tuples. The + symbol adds numbers, glues strings and joins lists, while * multiplies numbers and repeats strings and lists. Each type supplies its own meaning for the same symbol through the dunder methods from the previous lesson. That is also why your own classes can join in: define __len__ or __add__ and the built-in syntax works on them.

Why not just check the type?

A tempting alternative is a chain of isinstance() checks. This is plain text on purpose, because it shows the style to avoid:

# every new payment class means editing this function
def pay_all(methods, amount):
    for method in methods:
        if isinstance(method, CreditCard):
            print("card:", method.pay(amount))
        elif isinstance(method, GiftVoucher):
            print("voucher:", method.pay(amount))
        # a new BankTransfer class would be silently ignored

# works for today's classes and for every future one
def pay_all(methods, amount):
    for method in methods:
        print(method.pay(amount))

The chain needs a new branch for each class, and a class you forget is ignored without any error. When the object knows how to do the job, the caller can just ask. Keep isinstance() for cases where the objects genuinely cannot decide for themselves, such as checking input at the edge of your program.

Look before you leap, or just try it

Sometimes you really do not know whether an object supports an operation. Python programmers have two idioms for that:

  • LBYL, "look before you leap": check first with hasattr(obj, "send").
  • EAFP, "easier to ask forgiveness than permission": call the method and handle AttributeError if it is missing.
Python
class EmailSender:
    def send(self, text):
        return f"emailed: {text}"

class Pager:
    def send(self, text):
        return f"paged: {text}"

class Fax:
    pass          # no send() method

for device in [EmailSender(), Fax(), Pager()]:
    if hasattr(device, "send"):
        print(device.send("Server restarted"))
    else:
        print(type(device).__name__, "cannot send")

for device in [Fax(), Pager()]:
    try:
        print(device.send("Disk full"))
    except AttributeError:
        print(type(device).__name__, "has no send()")

Python programmers lean towards the second style, but keep the try block small. If send() itself contains a typo that raises AttributeError, a wide try/except reports "no send()" and hides the real bug.

The price of duck typing

Nobody checks the "contract" before the program runs. A misspelt method name or a different parameter list only shows up as an error when that line executes. So keep method names and parameters identical across your classes. When you want Python to enforce the contract up front, abstract base classes (a later lesson) can do that.

4Python Polymorphism: syntax

# 1) Polymorphism through a class family (overriding)
class Base:
    def action(self, data): ...
class ChildA(Base):
    def action(self, data): ...        # same name, its own behaviour
class ChildB(Base):
    def action(self, data):
        return super().action(data)    # or build on the parent's version

# 2) Duck typing: no shared parent needed
class Unrelated:
    def action(self, data): ...

for obj in [ChildA(), ChildB(), Unrelated()]:
    obj.action(data)                   # Python only asks: does obj have action()?

# 3) When you are not sure an object has the method
hasattr(obj, "action")                 # look before you leap
try:
    obj.action(data)                   # or just try it ...
except AttributeError:
    ...                                # ... and handle the failure

5Python Polymorphism: run the example

Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.

Interactive example
class PaymentMethod:
    def pay(self, amount):
        return f"{amount:.2f} paid"

class CreditCard(PaymentMethod):
    def pay(self, amount):
        return f"{amount * 1.03:.2f} charged to card"

class GiftVoucher:                    # no parent class
    def __init__(self, balance):
        self.balance = balance

    def pay(self, amount):
        used = min(amount, self.balance)
        self.balance -= used
        return f"{used:.2f} taken from voucher"

wallet = [PaymentMethod(), CreditCard(), GiftVoucher(30)]
for method in wallet:
    print(method.pay(50))
print(isinstance(wallet[2], PaymentMethod))
Expected output50.00 paid
51.50 charged to card
30.00 taken from voucher
False

7Python Polymorphism: how the example works

Hover or tap a step to highlight its lines in the example above.

  1. Line 1-3PaymentMethod is the parent class. Its pay() method returns a plain payment message.
  2. Line 5-7CreditCard inherits from PaymentMethod and overrides pay() with the same name and parameters but different behaviour: it adds a 3% fee.
  3. Line 9-11GiftVoucher has no parent class and keeps its own balance. It is not related to the other two.
  4. Line 13-16It still has a pay(amount) method with the same shape, so it counts as a valid payment method for duck typing. It takes what the balance allows and updates the balance.
  5. Line 18One list holds three objects of three different classes.
  6. Line 19-20The loop does not check any types. method.pay(50) runs the parent's version, then the child's version (50 times 1.03 is 51.50), then the voucher's version (only 30 is available).
  7. Line 21isinstance confirms that the voucher is not a PaymentMethod, and it worked in the loop anyway. Having the method is what mattered.

8Python Polymorphism: common mistakes

Giving the same job different method names

✗ Wrong

class EmailSender:
    def send(self, message):
        return f"Email: {message}"

class SmsSender:
    def send_sms(self, message):
        return f"SMS: {message}"

for channel in [EmailSender(), SmsSender()]:
    print(channel.send("Hello"))
AttributeError: 'SmsSender' object has no attribute 'send'

✓ Right

class EmailSender:
    def send(self, message):
        return f"Email: {message}"

class SmsSender:
    def send(self, message):
        return f"SMS: {message}"

for channel in [EmailSender(), SmsSender()]:
    print(channel.send("Hello"))

Polymorphism only works when every class offers the call under the same name. Pick one name for the job (send) and use it in all the classes, whether or not they share a parent.

Methods with the same name but different parameters

✗ Wrong

class EmailSender:
    def send(self, message):
        return f"Email: {message}"

class PushSender:
    def send(self):
        return "Push: default text"

for channel in [EmailSender(), PushSender()]:
    print(channel.send("Hello"))
TypeError: PushSender.send() takes 1 positional argument but 2 were given

✓ Right

class EmailSender:
    def send(self, message):
        return f"Email: {message}"

class PushSender:
    def send(self, message):
        return f"Push: {message}"

for channel in [EmailSender(), PushSender()]:
    print(channel.send("Hello"))

The shared name is only half of the contract: every class must also accept the same arguments. Give PushSender.send the message parameter, even if it decides to use it differently.

Handling each class in an isinstance() chain

✗ Wrong

class EmailSender:
    def send(self, message):
        return f"Email: {message}"

class SmsSender:
    def send(self, message):
        return f"SMS: {message}"

class PushSender:
    def send(self, message):
        return f"Push: {message}"

def deliver(channel, message):
    if isinstance(channel, EmailSender):
        return channel.send(message)
    elif isinstance(channel, SmsSender):
        return channel.send(message)

print(deliver(PushSender(), "Hello"))
Prints None. There is no error: the new class falls through every branch

✓ Right

class EmailSender:
    def send(self, message):
        return f"Email: {message}"

class SmsSender:
    def send(self, message):
        return f"SMS: {message}"

class PushSender:
    def send(self, message):
        return f"Push: {message}"

def deliver(channel, message):
    return channel.send(message)

print(deliver(PushSender(), "Hello"))

The chain has to be edited for every new class, and a class you forget is ignored silently. Let the objects do the work: call channel.send(message) directly and delete the chain.

Wrapping too much code in try/except AttributeError

✗ Wrong

class Pager:
    def __init__(self, number):
        self.number = number

    def send(self, message):
        return f"Page {self.numbr}: {message}"

try:
    print(Pager(42).send("Server down"))
except AttributeError:
    print("Pager has no send()")
Prints "Pager has no send()" even though it has one. The real AttributeError is the typo self.numbr

✓ Right

class Pager:
    def __init__(self, number):
        self.number = number

    def send(self, message):
        return f"Page {self.number}: {message}"

pager = Pager(42)
send = getattr(pager, "send", None)
if send is None:
    print("Pager has no send()")
else:
    print(send("Server down"))

The except caught an AttributeError raised inside send() and mislabelled it. Look the method up on its own (getattr(obj, "send", None) or hasattr), then call it outside the try, so real bugs still show their traceback. The typo is also fixed in the code below.

9Python Polymorphism: exercises

Write your answer, then press Check. The checks run in your browser.

1. Broadcast to any sender

Write code Easy

EmailSender is given. Do two things:

  1. Write a class SmsSender with a method send(message) that returns "SMS: <message>".
  2. Write broadcast(senders, message) that returns a list with the result of send(message) for every object in senders, in order. It must work for any object that has a send method, even one that is not one of your classes.
Your code
class EmailSender:
    def send(self, message):
        return f"Email: {message}"

# write class SmsSender here


def broadcast(senders, message):
    pass


print(broadcast([EmailSender()], "Hello"))

2. Predict the headers

Predict the output Medium

header() is only written in Report and Memo, but it calls self.title(). Type exactly what the program prints.

Read this code
class Report:
    def title(self):
        return "Report"

    def header(self):
        return f"== {self.title()} =="

class SalesReport(Report):
    def title(self):
        return "Sales " + super().title()

class Memo:
    def header(self):
        return "-- memo --"

for doc in [Report(), SalesReport(), Memo()]:
    print(doc.header())
print(isinstance(Memo(), Report), hasattr(Memo(), "title"))

2 more exercises → 5 more examples

Finished this lesson?
Progress is saved in this browser — create a free account to keep it across devices.

Questions learners ask

What is polymorphism in Python?

Polymorphism means one method call or operator can behave differently depending on the object it is used on. Calling send() on an EmailSender and an SmsSender runs different code, and len() or + give different results for lists, strings and numbers. You write one loop or function and it works with all of those objects.

What is duck typing in Python?

Duck typing means Python cares about what an object can do, not what class it is. If an object has the method you call, with compatible arguments, it works, even when it shares no parent class with the other objects. The name comes from the saying: if it walks like a duck and quacks like a duck, it is a duck.

Do I need inheritance to use polymorphism in Python?

No. Inheritance with method overriding is one way to get polymorphism, but duck typing lets completely unrelated classes be used interchangeably as long as they offer the same method names. Use inheritance when classes share real code or a genuine is-a relationship, and plain duck typing when they only share behaviour.

Further reading

10Next lesson

Up nextPython Multiple Inheritance and MROMultiple inheritance means a class lists more than one parent, such as class Duck(Swimmer, Flyer); Python then follows …

Related:
Python Dunder (Magic) MethodsPython InheritancePython Abstract Classes (ABC)

← Python Inheritance · All Object-Oriented Python lessons · Practise in the IDE