Polymorphism & Abstraction
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
One loop can send an email, a text, and a score alert without asking which kind of object it is holding. Polymorphism means "many forms": different objects can answer the same method call in their own way. Abstraction means focusing on the useful promise an object makes while ignoring the hidden details of how it keeps that promise.
Call the behavior, not the type
Python code often cares less about what class an object came from and more about what the object can do. Duck typing is Python's habit of trusting an object if it has the method or attribute your code needs, from the saying "if it quacks like a duck."
Think of a row of devices with the same Play button. A phone, a radio, and a laptop are different machines, but your finger can press Play on each one. The button is the promise. The machine decides the details.
class EmailNotice:
def __init__(self, player, address):
self.player = player
self.address = address
def send(self):
return f"Email to {self.address}: {self.player}, your quest is ready"
class TextNotice:
def __init__(self, player, phone):
self.player = player
self.phone = phone
def send(self):
return f"Text to {self.phone}: {self.player}, your quest is ready"
class ScoreNotice:
def __init__(self, player, score):
self.player = player
self.score = score
def send(self):
return f"Scoreboard: {self.player} reached {self.score} points"
def deliver_all(notices):
for notice in notices:
print(notice.send())
inbox = [
EmailNotice("Mina", "mina@example.com"),
TextNotice("Ravi", "555-0102"),
ScoreNotice("Noor", 420),
]
deliver_all(inbox)Email to mina@example.com: Mina, your quest is readyText to 555-0102: Ravi, your quest is readyScoreboard: Noor reached 420 points
deliver_all does not ask whether each object is an EmailNotice, a TextNotice, or a ScoreNotice. It asks one question: "Can you run send()?"
That is duck typing. These classes do not share a custom parent class. They share a behavior shape: a no-argument send method that returns text ready to print.
Dynamic dispatch picks the method
Dynamic dispatch is Python choosing which method body to run at runtime, which means while the program is running, based on the actual object receiving the method call.
In notice.send(), the variable name is always notice. The list name is always inbox. Still, Python runs three different send methods because the objects in the list are different.
class QuietReminder:
def __init__(self, player, task):
self.player = player
self.task = task
def message(self):
return f"{self.player}, remember: {self.task}"
class LoudReminder:
def __init__(self, player, task):
self.player = player
self.task = task
def message(self):
return f"{self.player.upper()}! DO THIS NOW: {self.task.upper()}"
def show(reminder):
print(reminder.message())
today = QuietReminder("Mina", "water the basil")
show(today)
today = LoudReminder("Ravi", "submit the score")
show(today)Mina, remember: water the basilRAVI! DO THIS NOW: SUBMIT THE SCORE
The name today moves from one object to another, just like the name tags from the variables lesson. The method call does not stick to the variable name. It follows the object currently wearing that name tag.
This is why polymorphism makes code smaller. show does not need a branch for every reminder class. Each object carries its own answer to message().
Try it - add one sender
Run this, then add a PushNotice class with a send method. Put one PushNotice("Ada", "Practice at 7") into outbox and run it again.
Name the interface
An interface is the small public promise outside code depends on: method names, arguments, return shape, and meaning. In the sender examples, the interface is "has a send() method that takes no extra arguments and returns message text."
You can define an interface in Python with plain documentation and tests: write the methods your function expects, keep the names consistent, and make every class honor the same meaning.
class ScoreReport:
def __init__(self, player, score):
self.player = player
self.score = score
def lines(self):
return [
f"Player: {self.player}",
f"Score: {self.score}",
]
class MoodReport:
def __init__(self, player, mood):
self.player = player
self.mood = mood
def lines(self):
return [
f"Player: {self.player}",
f"Mood: {self.mood}",
]
def print_report(report):
for line in report.lines():
print(line)
print("-")
print_report(ScoreReport("Mina", 120))
print_report(MoodReport("Ravi", "focused"))Player: MinaScore: 120-Player: RaviMood: focused-
print_report depends on the lines() interface. It does not need to know where the lines came from, whether the object stores a score or a mood, or which class name is on the blueprint.
That is abstraction doing useful work. You keep the outside promise small, so the inside details can change without breaking every caller.
Make the promise explicit with abc
Sometimes an informal interface is enough. Sometimes you want Python to reject a class that claims to be a report but forgets a required method.
An abstract base class is a parent class that names required methods and cannot be fully used until a subclass fills them in. The abc module is Python's standard-library tool for abstract base classes.
A decorator is syntax that adjusts a function or method; decorators get their full story in lesson 12.2. For now, read @abstractmethod as "subclasses must provide this method."
from abc import ABC, abstractmethod
class Report(ABC):
@abstractmethod
def lines(self):
pass
def preview(self):
return " | ".join(self.lines())
class ScoreReport(Report):
def __init__(self, player, score):
self.player = player
self.score = score
def lines(self):
return [
f"Player: {self.player}",
f"Score: {self.score}",
]
class DraftReport(Report):
pass
report = ScoreReport("Noor", 305)
print(report.preview())
try:
DraftReport()
except TypeError as error:
print(f"Rejected: {error}")Player: Noor | Score: 305Rejected: Can't instantiate abstract class DraftReport without an implementation for abstract method 'lines'
Report inherits from ABC, so Python treats it as an abstract base class. lines is abstract because it has @abstractmethod. preview is regular shared behavior: every real report can reuse it.
ScoreReport works because it implements lines. DraftReport fails because it does not. The ABC catches the missing method when you try to create the object.
Keep interfaces small
An interface should be boring and narrow. If outside code only needs send(), do not make every sender promise connect(), retry(), format_address(), and archive() too. Large interfaces force unrelated classes to pretend they have the same job.
Python gives you a practical ladder:
- Use duck typing when a small shared method is clear from context.
- Use an abstract base class when you want a named parent and a runtime check for required methods.
- Keep behavior promises separate from storage details. Callers should ask for
lines(), not reach into every object's private attributes.
The goal is not to make every class inherit from a grand parent class. The goal is to let code ask for the behavior it needs and let each object answer in its own form.
Checkpoint
Answer all three to mark this lesson complete
Polymorphism lets one call reach many object forms, and abstraction keeps that call focused on a small promise. Next, Python lets classes combine behavior from more than one parent, which is powerful enough to need its own rules.