Learn by reading through in order

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.

Three Classes Override calculate_salary
Employee(parent)base_salary(unchanged)= 300kManager(child override)base +team x 50k= 1.2MEngineer(child override)base +skill x 20k= 380k
The parent 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

Define Employee / Manager / Engineer and confirm each calculate_salary returns a different result.

① Build the three classes following the example above (Manager and Engineer should call super().__init__(name, base_salary) to delegate parent init).

② Build Employee("Alice", 300000), Manager("Bob", 800000, 8), and Engineer("Carol", 300000, 4), then print each calculate_salary() result.

(Once it runs correctly, the explanation will appear.)

Python Editor

Run code to see output
How emp.calculate_salary() Behaves Inside a Loop
emp.calculate_salary()(same line)emp = alice(Employee)-> 300kemp = bob(Manager)-> 1.2Memp = carol(Engineer)-> 380kEmployeeManagerEngineer
The loop body is just emp.calculate_salary() on a single line, but a different method gets picked automatically depending on 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
PayrollSystem Doesn't Care What's Inside
PayrollSystem
  • employees = [...] (mixed types)
  • for emp in self.employees: walks them
  • Just call emp.calculate_salary()
Employee (default)
  • returns base_salary
Manager
  • base + team * 50k
Engineer
  • base + skill * 20k
Even with three different classes mixed into the list, PayrollSystem just calls the same method name and the right calculation runs per-type.

Reuse the three classes from Practice 1 to build a PayrollSystem that calculates the total salary (the console keeps state, so previous class definitions are still there).

① Define class PayrollSystem: with __init__ that initializes self.employees = []. Implement add(self, employee) and total(self) (have total walk a for loop summing each emp.calculate_salary()).

② Build payroll = PayrollSystem(), then add three employees: Employee("Alice", 300000) / Manager("Bob", 800000, 8) / Engineer("Carol", 300000, 4).

③ Loop over payroll.employees printing emp.name and emp.calculate_salary() line by line, then print payroll.total() with the prefix "total:".

Python Editor

Run code to see output

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
Type Branches vs Polymorphism
BAD: type branchingif type ==x N timesevery new type= new branchGOOD: polymorphicemp.calculate()new type =just a classripplecontained
Branch-on-type code piles 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

Polymorphic Design vs Type-Branching Design
Polymorphic design
  • 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
Type-branching design
  • 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
Same requirement, two designs — but how much code the caller writes and how far changes ripple can vary dramatically.
QUIZ

Knowledge Check

Answer each question one by one.

Q1Which is the most accurate description of polymorphism?

Q2Polymorphism tends to eliminate which structure from the caller's code?

Q3Which is the best description of Python's duck typing?