Ran Wei / Object-Oriented Design / Module 4
中文
Object-Oriented Design — Ran Wei

Module 4: Polymorphism

One interface, many implementations — how the same method call produces different behaviour depending on the object’s type, from runtime dispatch to operator overloading.

Prereq: OOD Module 3 Python 3 Duck Typing Dunder Methods Open/Closed SysML v2 Bridge
1

What Is Polymorphism?

Polymorphism (from Greek: “many forms”) is the ability of different objects to respond to the same message in their own way. You call the same method on different objects and each does something appropriate for its own type — without the calling code needing to know which type it is dealing with.

TypeHow Python achieves it
Subtype polymorphismOverriding methods in subclasses. The runtime type determines which version runs.
Duck typingAny object that implements the expected methods can be used, regardless of its class hierarchy.
Operator overloadingDunder methods (__add__, __lt__, etc.) give standard operators object-specific meanings.
ℹ️ The Key Insight
Polymorphism decouples the calling code from the concrete type. A function that accepts a Book works correctly with any current or future subtype, without modification. This is what makes OOD code extensible.
2

Method Overriding Revisited

When Python calls a method, it looks up the method on the actual type of the object at runtime — not the declared type of the variable. This is dynamic dispatch:

Python
catalogue: list[Book] = [
    DigitalBook("978-1-11", "Clean Code",    "Martin",  29.99, "PDF", 5.2, "..."),
    PhysicalBook("978-1-22", "Refactoring",  "Fowler",  39.99, 5, 520, "...", "WH1"),
    DigitalBook("978-1-33", "Pragmatic Programmer", "Hunt", 34.99, "EPUB", 8.4, "..."),
    PhysicalBook("978-1-44", "SICP",         "Abelson", 24.99, 3, 680, "...", "WH2"),
]

# Same call, different behaviour per type — dynamic dispatch
for book in catalogue:
    print(book.get_details())            # dispatches to Digital or Physical
    print(f"  Available: {book.is_available()}")  # inherited — same for all
Book hierarchy with polymorphic methods
Figure 1 — The Book hierarchy with polymorphic methods. Both subclasses override get_details(); Book provides __str__, __lt__, __eq__ so all subtypes inherit comparison and string behaviour automatically.
3

Duck Typing

Python does not require objects to share a common base class for polymorphic use. Any object that has the right methods can be used — “if it walks like a duck and quacks like a duck, it is a duck.”

Python
# A completely independent class — not a Book subclass
class GiftCard:
    def __init__(self, code: str, value: float):
        self.code = code; self.value = value; self.stock = 1

    def get_details(self) -> str:
        return f"Gift Card {self.code} | £{self.value:.2f}"

    def is_available(self) -> bool:
        return self.stock > 0


# print_catalogue only cares about get_details() and is_available()
def print_catalogue(items) -> None:
    for item in items:
        print(item.get_details())
        if not item.is_available():
            print("  *** OUT OF STOCK ***")


mixed = [
    DigitalBook("978-1-11", "Clean Code", "Martin", 29.99, "PDF", 5.2, "..."),
    GiftCard("GC-2024-001", 25.00),
]
print_catalogue(mixed)   # works with both — no inheritance needed
💡 Duck Typing
print_catalogue() works with any object that has get_details() and is_available(), regardless of class hierarchy. This is the most flexible form of polymorphism in Python and makes functions trivially easy to test with simple stub objects.

isinstance() vs Duck Typing

PatternWhen to use it
Duck typing (just call the method)You only need the common interface. Preferred default.
isinstance(x, DigitalBook)You need a method only DigitalBook has (e.g. download()). Use sparingly.
isinstance(x, Book)You need to confirm it's a Book before calling Book-specific methods. Acceptable for validation.
4

Special Methods: __str__, __repr__, __len__

Python uses dunder methods to integrate objects with the language itself. These are a form of polymorphism — the same built-in function behaves differently depending on the object it receives:

Python
# Add to Book
def __str__(self) -> str:
    return self.get_details()   # delegates to the overridden method

def __repr__(self) -> str:
    return (f"{self.__class__.__name__}(isbn={self.isbn!r}, "
            f"title={self.title!r}, price={self.price!r})")


# Add to ShoppingCart
def __len__(self) -> int:    return len(self._items)
def __contains__(self, b): return b in self._items
def __iter__(self):         return iter(self._items)


# Usage
book = DigitalBook("978-1-11", "Clean Code", "Martin", 29.99, "PDF", 5.2, "...")
print(book)          # "Clean Code by Martin | PDF (5.2 MB) £29.99"
repr(book)           # "DigitalBook(isbn='978-1-11', title='Clean Code', ...)"
print(len(cart))     # 1
print(book in cart)  # True
for b in cart: print(b)
ℹ️ __str__ vs __repr__
__str__ is for end users: readable and concise. __repr__ is for developers: precise enough to reconstruct the object. When only implementing one, prefer __repr__ — Python uses it as a fallback for __str__ in the REPL and in containers like lists.
5

Operator Overloading

Python lets you define the behaviour of standard operators (==, <, +, in) for your own types. This is operator overloading: the same operator behaves differently depending on the types involved.

Python
# Add to Book
def __eq__(self, other) -> bool:
    if not isinstance(other, Book): return NotImplemented
    return self.isbn == other.isbn

def __hash__(self) -> int:   # required when __eq__ is defined
    return hash(self.isbn)

def __lt__(self, other) -> bool:
    if not isinstance(other, Book): return NotImplemented
    return self.price < other.price

# ShoppingCart merge
def __add__(self, other: "ShoppingCart") -> "ShoppingCart":
    if not isinstance(other, ShoppingCart): return NotImplemented
    merged = ShoppingCart(f"{self.cart_id}+{other.cart_id}")
    for b in self: merged.add_item(b)
    for b in other: merged.add_item(b)
    return merged
Python
# Standard Python idioms now work naturally:

books.sort()          # uses __lt__ to sort by price
for b in books: print(f"  £{b.price:.2f}  {b.title}")
# £24.99  SICP
# £29.99  Clean Code
# £39.99  Refactoring

b1 = DigitalBook("978-1-11", "Clean Code", "Martin", 29.99, "PDF",  5.2, "...")
b2 = DigitalBook("978-1-11", "Clean Code", "Martin", 29.99, "EPUB", 6.0, "...")
print(b1 == b2)       # True — same ISBN, different format

unique_books = {b1, b2}
print(len(unique_books)) # 1  (requires __hash__)

merged = cart_a + cart_b
print(len(merged))   # len(cart_a) + len(cart_b)
Book and ShoppingCart with operator overloading
Figure 2Book and ShoppingCart with operator overloading. Book gains __str__, __repr__, __lt__, __eq__, __hash__. ShoppingCart gains __len__, __contains__, __iter__, __add__ — making it behave like a native Python sequence.
⚠️ Pitfall — Forgetting __hash__
Defining __eq__ automatically sets __hash__ to None, making the class unhashable (cannot be used in sets or as dict keys). Always define __hash__ alongside __eq__. A safe default: return hash(self.isbn).
6

Polymorphic Functions

The real payoff of polymorphism: functions that work correctly with any current or future Book subtype, without modification.

Python
from typing import Sequence

# 1. Apply a discount to any collection of books
def apply_sale(books: Sequence[Book], discount: float) -> None:
    for book in books:
        book.apply_discount(discount)
        print(f"  {book.title} -> £{book.price:.2f}")


# 2. Split a mixed catalogue by type
def split_catalogue(books):
    digital  = [b for b in books if isinstance(b, DigitalBook)]
    physical = [b for b in books if isinstance(b, PhysicalBook)]
    return digital, physical


# 3. Total shipping for physical books only
def total_shipping(books: Sequence[Book]) -> float:
    return sum(
        b.get_shipping_cost()
        for b in books
        if isinstance(b, PhysicalBook)
    )


apply_sale(catalogue, 15)
digital, physical = split_catalogue(catalogue)
print(f"Shipping: £{total_shipping(physical):.2f}")
💡 Open/Closed Principle
apply_sale() is open for extension (adding AudioBook works automatically) and closed for modification (you never change apply_sale() when adding a new subtype). This is the Open/Closed Principle — one of the SOLID principles covered in Module 7.
7

SysML v2 Bridge

In SysML v2, polymorphism arises naturally from the subclassification and redefinition operators introduced in Module 3. An action declared on an abstract part def is automatically polymorphic:

SysML v2
abstract part def Book {
    abstract action def get_details : String;  // polymorphic
    action def is_available : Boolean;
}

part def DigitalBook :> Book {
    :>> get_details : String;   // redefines — dispatched at runtime
}

part def PhysicalBook :> Book {
    :>> get_details : String;   // different redefinition
}

// catalogue holds any Book subtype — polymorphic collection
part catalogue : Book[0..*];
Python / OOD
SysML v2
Polymorphic method call
abstract action def + :>> redefinitions
Duck typing
Structural conformance / classifier compatibility
isinstance(x, Book)
Type membership via subclassification chain
Polymorphic function
Action operating on Book[*] multiplicity
ℹ️ Polymorphism in MBSE
A behaviour defined on an abstract block or part def is automatically available to all specialisations. This lets SysML v2 models express that a system can handle any component satisfying an interface — directly analogous to duck typing at the modelling level.
8

Summary

Dynamic Dispatch

Python resolves method calls at runtime on the actual type of the object — not the declared type of the variable.

Duck Typing

Any object with the right methods can be used polymorphically. No shared base class required. The most flexible form.

__str__ / __repr__

__str__ for users, __repr__ for developers. When only implementing one, prefer __repr__.

__eq__ + __hash__

Always define __hash__ alongside __eq__ to keep objects usable in sets and dicts.

Open/Closed

Polymorphic functions are open for extension, closed for modification. Adding a new subtype never requires changing existing code.

SysML v2

Polymorphism via abstract action def + :>> redefinitions. part catalogue : Book[*] is a polymorphic collection.

🧱
Next — Module 5

Composition & Aggregation

How objects are assembled from other objects — the has-a relationship in depth. We build Order, OrderItem, and Payment, explore composition vs aggregation, and map both to SysML v2 part and reference semantics.