Polymorphism
How the same method call produces different behaviour, duck typing in Python, operator overloading, and the full polymorphism story in UML and SysML v2.
Reuse and specialise behaviour through class hierarchies — extending Book into DigitalBook and PhysicalBook, and introducing abstract classes.
Inheritance models an “is-a” relationship. A DigitalBook is a Book. A PremiumCustomer is a Customer. The child class inherits all attributes and methods of the parent and can add its own or override inherited ones.
| Relationship | Use |
|---|---|
| Is-a | Inheritance. DigitalBook is-a Book — shares all Book behaviour and adds its own. |
| Has-a | Composition. ShoppingCart has Books — it owns references to Book objects but is not itself a Book. |
Our bookshop sells two kinds of book: digital downloads and physical copies. Both share core Book attributes, but each has its own additional properties. We first make Book abstract so it cannot be instantiated directly:
from abc import ABC, abstractmethod class Book(ABC): """Abstract base class for all book types.""" def __init__(self, isbn, title, author, price, stock=0): if not isbn.strip(): raise ValueError("ISBN cannot be blank") self.isbn = isbn; self.title = title; self.author = author self._price = 0.0; self._stock = 0 self.price = price; self.stock = stock @abstractmethod def get_details(self) -> str: """Every subclass must provide its own description.""" ... def is_available(self): return self.stock > 0 def apply_discount(self, percent): if not 0 <= percent <= 100: raise ValueError("Discount must be 0–100") self.price *= (1 - percent / 100) class DigitalBook(Book): """A downloadable e-book.""" def __init__(self, isbn, title, author, price, file_format, file_size, download_url): super().__init__(isbn, title, author, price, stock=999) self.file_format = file_format self.file_size = file_size self.download_url = download_url def get_details(self) -> str: return (f"{self.title} by {self.author} | {self.file_format} " f"({self.file_size:.1f} MB) £{self.price:.2f}") def download(self): print(f"Downloading from {self.download_url}") class PhysicalBook(Book): """A printed book stored in a warehouse.""" SHIPPING_RATE = 0.005 # £ per gram def __init__(self, isbn, title, author, price, stock, weight, dimensions, warehouse): super().__init__(isbn, title, author, price, stock) self.weight = weight; self.dimensions = dimensions self.warehouse = warehouse def get_details(self) -> str: return (f"{self.title} by {self.author} | Paperback " f"£{self.price:.2f} ({self.weight}g)") def get_shipping_cost(self) -> float: return round(self.weight * self.SHIPPING_RATE, 2)
Book with two concrete subclasses. The hollow arrowhead denotes generalisation (inheritance). «abstract» marks the base class and the abstract method.When a subclass defines __init__, it must call super().__init__() to ensure the parent class is properly initialised. Without it, all the Book invariants and property setup are skipped:
digital = DigitalBook( isbn="978-0-13-468599-1", title="The Pragmatic Programmer", author="Hunt & Thomas", price=34.99, file_format="EPUB", file_size=8.4, download_url="https://books.example.com/pp.epub" ) print(digital.isbn) # "978-0-13-468599-1" print(digital.is_available()) # True (stock=999) print(digital.get_details()) # "The Pragmatic Programmer | EPUB (8.4 MB) £34.99" digital.download() # "Downloading from https://..." digital.apply_discount(10) print(digital.price) # 31.491
super() does not mean “call the parent class directly.” It calls the next class in the Method Resolution Order (MRO) — the linearised list Python uses to resolve method lookups. For simple single-inheritance chains this is always the direct parent, but the distinction matters for multiple inheritance.
Both DigitalBook and PhysicalBook override get_details(). The method name is the same; the implementation differs. Python selects the correct version at runtime based on the actual type of the object:
books = [ DigitalBook("978-1-11", "Clean Code", "Martin", 29.99, "PDF", 5.2, "..."), PhysicalBook("978-1-22", "Refactoring", "Fowler", 39.99, 5, 520, "...", "WH1"), ] for book in books: print(book.get_details()) # correct version called automatically # Clean Code by Martin | PDF (5.2 MB) £29.99 # Refactoring by Fowler | Paperback £39.99 (520g)
This is polymorphism — the same method call produces different behaviour depending on the object's type. Module 4 covers polymorphism in depth.
Book must work equally with a DigitalBook or PhysicalBook.
An abstract class cannot be instantiated directly — it exists only to be subclassed. @abstractmethod forces every concrete subclass to provide its own implementation:
# Cannot instantiate the abstract class: book = Book("978-0", "Test", "Author", 9.99) # TypeError: Can't instantiate abstract class Book # with abstract method get_details # A subclass that forgets get_details also fails: class BrokenBook(Book): pass broken = BrokenBook("978-0", "Test", "Author", 9.99) # TypeError: Can't instantiate abstract class BrokenBook
@abstractmethod is a contract: “Every concrete Book must know how to describe itself.” The abstract class defines the interface; each subclass provides the implementation. This is the foundation of polymorphism.
The same pattern applied to Customer:
class PremiumCustomer(Customer): """A customer enrolled in the loyalty programme.""" def __init__(self, customer_id, name, email, discount_rate=0.10): super().__init__(customer_id, name, email) if not 0 <= discount_rate <= 1: raise ValueError("Discount rate must be between 0 and 1") self._loyalty_points = 0 self._discount_rate = discount_rate def get_discount_rate(self): return self._discount_rate def add_points(self, points: int): if points <= 0: raise ValueError("Points must be positive") self._loyalty_points += points def get_details(self) -> str: return (f"{self.get_name()} (Premium) | " f"Points: {self._loyalty_points} | " f"Discount: {self._discount_rate*100:.0f}%")
Customer and PremiumCustomer. The hollow arrowhead denotes inheritance. PremiumCustomer inherits all Customer attributes and methods, then adds loyalty-programme behaviour.Inheritance is powerful but often overused. A useful heuristic: prefer composition over inheritance unless the is-a test is clearly satisfied and unlikely to change.
| Factor | Favour Inheritance | Favour Composition |
|---|---|---|
| Relationship | “Is-a”: DigitalBook is-a Book | “Has-a”: ShoppingCart has Books |
| Reuse scope | All instances need the parent's behaviour | Only some behaviours need sharing |
| Flexibility | Hierarchy is stable, unlikely to change | Behaviour may vary or reconfigure at runtime |
| Coupling | Tight coupling acceptable — subclass knows parent well | Looser coupling preferred — components are independent |
SysML v2 provides direct language constructs for inheritance. The mapping is precise:
| Python / OOD | SysML v2 |
|---|---|
class Child(Parent) | part def Child :> Parent { ... } |
super().__init__() | Implicit — inherited features available automatically |
| Override method | :>> methodName { ... } |
| Abstract class (ABC) | abstract part def Book { ... } |
@abstractmethod | abstract action def get_details : String; |
abstract part def Book { attribute isbn : String; attribute title : String; attribute author : String; private attribute _price : Real := 0.0; abstract action def get_details : String; // must be redefined action def is_available : Boolean; action def apply_discount { in percent : Real; } } part def DigitalBook :> Book { attribute file_format : String; attribute file_size : Real; attribute download_url : String; :>> stock = 999; // redefine with fixed value :>> get_details : String; // override the abstract action action def download : Void; // new capability } part def PhysicalBook :> Book { attribute weight : Real; attribute dimensions : String; attribute warehouse : String; :>> get_details : String; action def get_shipping_cost : Real; }
part def Child :> Parent:>> methodNameabstract part defabstract action def:> (subclassification) declares “this part def specialises that one” — equivalent to class inheritance. :>> (redefinition) overrides a specific feature from the parent — equivalent to method overriding. Both are needed together: :> establishes the hierarchy, :>> customises individual features within it.
Use inheritance only when the subclass truly is a specialisation of the superclass and satisfies the Liskov Substitution Principle.
Always call super().__init__() in a subclass constructor to ensure the parent is fully initialised before adding subclass state.
Each subclass provides its own implementation. Python selects the correct version at runtime. The contract must be preserved.
ABC + @abstractmethod define an interface that all concrete subclasses must implement. Cannot be instantiated directly.
Prefer composition over inheritance when the is-a relationship is unclear, the hierarchy would grow deep, or runtime flexibility is needed.
:> declares subclassification; :>> redefines a feature. abstract part def and abstract action def enforce the abstract class pattern.
How the same method call produces different behaviour, duck typing in Python, operator overloading, and the full polymorphism story in UML and SysML v2.