Python Programming from First Principles: Learn to Code, Think Computationally, and Solve Real-World Problems
PDFEPUB
BOOK 000 · CHAPTER 7 · Polymorphism

Polymorphism

On this pageBuilt-in ExampleUnrelated ClassesOverridden MethodsSwappable Behavior with PoliciesCoding Challenge 1 - Class PolymorphismCoding Challenge 2 – Polymorphism with Inheritance (Shapes)

Polymorphism lets a caller use the same operation with different kinds of object. Each object supplies the behavior needed for that operation.

The UML class boxes show three separate classes with the same public operation. No inheritance line is drawn: these classes need not share a base to support status().

StudentUser+status()AdminUser+status()GuestUser+status()

Built-in Example

print(len("Hello"))               # counts characters
print(len(["a", "b", "c"]))       # counts list items
print(len({"a": 1, "b": 2}))      # counts dictionary pairs
Listing 317 — Built-in Example

Output

5
3
2

len counts characters, list elements or dictionary entries according to the object.

Unrelated Classes

class StudentUser:
    def status(self):
        print("Attends classes.")

class AdminUser:
    def status(self):
        print("Manages the system.")

class GuestUser:
    def status(self):
        print("Limited access.")
Listing 318 — Unrelated Classes

Defines StudentUser, AdminUser, GuestUser; no visible output.

All three classes provide status. They need no common base class for this use.

users = [StudentUser(), AdminUser(), GuestUser()]

for u in users:
    u.status()
Listing 319 — Unrelated Classes

Output

Attends classes.
Manages the system.
Limited access.

The loop calls the same method; each object prints its own response.

Overridden Methods

Run these definitions in order:

class User:
    def __init__(self, user_id, name, email):
        self.user_id = str(user_id)
        self.name = name
        self.email = email

    def role(self):
        return "user"
Listing 320 — Overridden Methods

Defines User; no visible output.

class Student(User):
    def __init__(self, user_id, name, email):
        super().__init__(user_id, name, email)
    def role(self):
        return "student"
Listing 321 — Overridden Methods

Defines Student; no visible output.

class Professor(User):
    def __init__(self, user_id, name, email):
        super().__init__(user_id, name, email)
        self.teaches = []

    def role(self):
        return "professor"
Listing 322 — Overridden Methods

Defines Professor; no visible output.

u1 = Student(101, "Anna", "anna@example.com")
u2 = Professor(44, "Dr. Singh", "singh@example.com")

for u in (u1, u2):
    print(u.role())
Listing 323 — Overridden Methods

Output

student
professor

The caller uses role() without checking whether the object is a student or professor.

Swappable Behavior with Policies

A policy is an object to which another object delegates a decision. First run the Schedule and Course definitions from Composition. The following classes reuse Course and provide two compatible format(course) operations.

The hollow triangle marks inheritance from Course; dashed arrows show possible dependencies on formatting policies. The two policy classes provide the same operation without inheriting from one another.

CourseCourseWithPolicy+policy+summary()SummaryPolicy+format(course)SummaryWithSchedule+format(course)may delegate tomay delegate to
class SummaryPolicy:
    def format(self, course):
        return f"{course.code} — {course.title} ({course.credits} cr)"

class SummaryWithSchedule:
    def format(self, course):
        return f"{course.code} — {course.title} ({course.credits} cr) [{course.schedule}]"

class CourseWithPolicy(Course):
    def __init__(self, code, title, credits, schedule=None, policy=None):
        super().__init__(code, title, credits, schedule)
        self.policy = policy or SummaryPolicy()  # composed behavior

    def summary(self):
        return self.policy.format(self)
Listing 324 — Swappable Behavior with Policies

Defines SummaryPolicy, SummaryWithSchedule, CourseWithPolicy; no visible output.

CourseWithPolicy inherits Course’s data handling but delegates summary formatting to its policy.

plain = CourseWithPolicy("NLP220", "Natural Language Processing", 6)
fancy = CourseWithPolicy("NLP220", "Natural Language Processing", 6,
                         policy=SummaryWithSchedule())

plain.add_meeting("Thu", "10:00-11:30")
fancy.add_meeting("Thu", "10:00-11:30")

print(plain.summary())
print(fancy.summary())
Listing 325 — Swappable Behavior with Policies

Output

NLP220 — Natural Language Processing (6 cr)
NLP220 — Natural Language Processing (6 cr) [Thu 10:00-11:30]

The plain policy omits meeting information; the other includes it. Matching names alone are insufficient: both policies must also accept the expected arguments and return suitable text.

Coding Challenge 1 - Class Polymorphism

Complete three handlers: print to the console, append to self.messages, and simulate remote delivery by printing. In process_logs, loop over handlers first, then messages. Call handle(message) without type checks.

Exercise starter: complete before running.

# Coding Challenge 1: Class Polymorphism (Strategy-style Logging)
class ConsoleHandler:
    def handle(self, message):
        # TODO: print message to console
        pass


class MemoryHandler:
    def __init__(self):
        # TODO: initialize self.messages to an empty list
        pass

    def handle(self, message):
        # TODO: append to internal list
        pass


class RemoteHandler:
    def handle(self, message):
        # TODO: "send" message to a remote server (simulate with print)
        pass


def process_logs(handlers, messages):
    # TODO: outer loop over handlers, inner loop over messages; call handle(message)
    pass
Listing 326 — Coding Challenge 1 - Class Polymorphism

Exercise starter: complete before running.

handlers = [ConsoleHandler(), MemoryHandler(), RemoteHandler()]
messages = ["User logged in", "Error: invalid password"]

process_logs(handlers, messages)
print("Stored messages:", handlers[1].messages)
Listing 327 — Coding Challenge 1 - Class Polymorphism

Target output for your completed solution

[Console] User logged in
[Console] Error: invalid password
[Remote] Sending: User logged in
[Remote] Sending: Error: invalid password
Stored messages: ['User logged in', 'Error: invalid password']

Coding Challenge 2 – Polymorphism with Inheritance (Shapes)

Implement area() for Circle, Rectangle and Triangle. Require non-negative dimensions. Make total_area sum the returned areas without inspecting concrete types.

Exercise starter: complete before running.

# Coding Challenge 2: Polymorphism with Inheritance (Shapes)
import math

class Shape:
    def area(self):
        # TODO: subclass should override
        raise NotImplementedError("Subclasses must implement area()")


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    # TODO: implement area()


class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    # TODO: implement area()


class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    # TODO: implement area()


def total_area(shapes):
    # TODO: sum up areas of all shapes in the list
    pass


# demo
shapes = [
    Circle(3), Rectangle(6, 5), Triangle(4, 8)
]

print("Total area:", round(total_area(shapes), 2))
Listing 328 — Coding Challenge 2 – Polymorphism with Inheritance (Shapes)

Target output for your completed solution

Total area: 74.27

The fixture has areas 9 * math.pi, 30 and 16. NotImplementedError reports a missing operation when called; it does not make Shape an abstract base class. Interfaces and behavioral contracts, discussed in the next chapter, make these expectations explicit.