Q1Which is the most accurate description of polymorphism?
Polymorphism — Same Method Name, Different Behavior per Type
Learn Python polymorphism. Override parent methods in subclasses so callers don't have to think about types, and replace if type(...) branches with clean OOP — all with diagrams.
Last time we covered multiple inheritance and MRO. To round out the OOP series, this article covers the third pillar — polymorphism.
What Is Polymorphism?
Polymorphism is the idea that the same interface (method name) can do different things depending on the type. Suppose you want a single "calculate salary" operation, but you want employees, managers, and engineers to use different formulas.
Define calculate_salary on the parent class Employee, and have the subclasses Manager and Engineer override it with their own formulas. Now the calling code just writes employee.calculate_salary() without caring which class is which, and the right calculation runs.
Employee defines calculate_salary. Subclasses Manager / Engineer override it with their own formulas. Same method name, different per-class results.Building on the Parent, Changing the Formula in Each Child
Let's actually build the salary example. Employee is the parent, with Manager (bumped by team size) and Engineer (bumped by skill level) as children. The key is that all three define a method with the same name calculate_salary.
class Employee:
def __init__(self, name, base_salary):
self.name = name
self.base_salary = base_salary
def calculate_salary(self): # default = base salary only
return self.base_salary
class Manager(Employee):
def __init__(self, name, base_salary, team_size):
super().__init__(name, base_salary)
self.team_size = team_size
def calculate_salary(self): # add bonus by team size
return self.base_salary + self.team_size * 50000
class Engineer(Employee):
def __init__(self, name, base_salary, skill_level):
super().__init__(name, base_salary)
self.skill_level = skill_level
def calculate_salary(self): # add bonus by skill level
return self.base_salary + self.skill_level * 20000
emp's class — that's the power of polymorphism.Bundling in a List — A Loop That Doesn't Care About Types
Polymorphism really shows its strength when you dump objects of different types into a single list and process them all together. Wrap the salary calculation in a PayrollSystem class and the loop body collapses to a single line.
class PayrollSystem:
def __init__(self):
self.employees = []
def add(self, employee):
self.employees.append(employee)
def total(self):
result = 0
for emp in self.employees: # types can vary
result += emp.calculate_salary() # same name works
return result
payroll = PayrollSystem()
payroll.add(Employee("Alice", 300000))
payroll.add(Manager("Bob", 800000, 8))
payroll.add(Engineer("Carol", 300000, 4))
print(payroll.total()) # 1880000
employees = [...](mixed types)for emp in self.employees:walks them- Just call emp.calculate_salary()
- returns base_salary
- base + team * 50k
- base + skill * 20k
PayrollSystem just calls the same method name and the right calculation runs per-type.What It Looks Like Without Polymorphism
Try to do the same thing without polymorphism and you end up writing if type(emp) == ...: branches. It works, but every new role means another if branch — and the moment you forget one, you've got a bug.
# (BAD) without polymorphism (branch on type)
def calc(emp):
if type(emp) is Manager:
return emp.base_salary + emp.team_size * 50000
elif type(emp) is Engineer:
return emp.base_salary + emp.skill_level * 20000
else:
return emp.base_salary
# (GOOD) with polymorphism (push the logic into the classes)
def calc(emp):
return emp.calculate_salary() # one line
if type into the caller and grows with every new type. Polymorphism keeps the caller a single line — adding a new type means just adding a class."The Caller Doesn't Care About Types" — That's the Mantra
A reliable test for whether your design is polymorphic: does the caller's code have piles of if type(...) or if isinstance(...) branches? If yes, the standard refactor is to move that branching into method overrides on the classes. "Add a class" and "add an if" tend to be a tradeoff.
Duck Typing — All You Need Is the Same Method Name
Python's polymorphism has a looser, more permissive flavor too: duck typing. The saying "if it walks like a duck and quacks like a duck, it's a duck" turns into "if a class has the right method, it doesn't matter what class it is."
In the example below, Cat and Dog don't share an Animal parent at all — but as long as both have a speak() method, the same function handles both. Python prioritizes "does it have the method?" over the inheritance graph.
class Cat:
def speak(self):
return "Meow"
class Dog:
def speak(self):
return "Woof"
def shout(animal): # the type isn't enforced
print(animal.speak())
shout(Cat()) # Meow
shout(Dog()) # Woof
Languages like Java or C# require a shared parent class to make polymorphism work. Python is happy as long as the method is there at call time. That gives you flexibility, but "making sure method names mean the same thing across classes" becomes the caller's responsibility — keep that in mind.
The Two Designs in One Slide
- Caller code — single line
emp.calculate_salary() - Adding a new type — just write a new class with
calculate_salary - Blast radius — stays inside the classes
- Readability — "same name, varies by type" is enough to follow
- Caller code — pile of
if type(emp) is ... - Adding a new type — every branch needs review
- Blast radius — leaks into the caller
- Readability — you re-read the branches every time
Knowledge Check
Answer each question one by one.
Q2Polymorphism tends to eliminate which structure from the caller's code?
Q3Which is the best description of Python's duck typing?