A complete walkthrough of polymorphism, Protocol-based structural subtyping, dataclasses, and polymorphic dispatch — explained step by step through a real airline ticketing simulator. Every class, every method, every design decision explained.
Imagine stepping up to an airline booking counter. You can pay with PayPal, a credit card, or cash at the airport. Your ticket can be printed as a clean one-liner summary or as a full e-ticket with every detail. The booking agent does not care which — they process your payment and hand you your ticket regardless. This is exactly what Ticketing.py simulates. And the mechanism that makes the booking agent indifferent to which payment method or ticket format you choose is one of the most important concepts in all of object-oriented programming: polymorphism.
Polymorphism means many forms. In Python, it means that a single piece of code — a method call like self.payment.confirm_payment() — can trigger completely different behaviour depending on which object is behind self.payment at runtime. The Booking class does not need an if/elif chain to check whether payment is a PayPal, CreditCard, or CashAtAirport object. It simply calls confirm_payment() and Python dispatches to the right implementation automatically. That is polymorphism in action.
This program implements polymorphism using Python's Protocol class from the typing module. Unlike abstract base classes, Protocols do not require explicit inheritance — any class that implements the right method signatures automatically satisfies the Protocol. This is called structural subtyping or duck typing. Combined with the @dataclass decorator, which eliminates boilerplate constructor code, this program is a focused, practical demonstration of modern Python OOP design.
Protocol, duck typing and structural subtyping, the @dataclass decorator and __post_init__, polymorphic dispatch eliminating isinstance checks, the open/closed principle in practice, route-based fare calculation with a dictionary lookup, and the if __name__ == "__main__" entry guard — all demonstrated in one cohesive, runnable program.
datetime, dataclasses, and typing from the standard library are used throughout.
Ticketing.py is built around two Protocols and four concrete classes, all orchestrated by a Booking dataclass and a main() function. Before diving into individual sections, understanding the full picture of how these pieces relate to each other — and how data flows from user input to a printed ticket — is essential.
name() → str and confirm_payment(amount, description) → bool. No inheritance required — any class with these methods qualifies.generate(booking) → str. Allows the Booking class to generate tickets without knowing whether the format is simple or detailed.PaymentMethod Protocol by implementing both required methods with different logic: redirect URL, 3D Secure simulation, and cash counter instructions.TicketPresenter. One returns a compact ASCII-art receipt; the other returns a full e-ticket with contact, full date, and base fare breakdown.__post_init__ to auto-generate ticket_id, look up base_fare from a routes dictionary, and calculate total. Calls both Protocols via confirm_and_issue().import datetime from dataclasses import dataclass from typing import Protocol
All three modules are part of Python's standard library — no pip install is needed. datetime provides date arithmetic for the default departure date. dataclass is a decorator that auto-generates __init__, __repr__, and __eq__ from annotated class fields. Protocol enables structural subtyping — defining interfaces that classes satisfy implicitly by implementing the right methods.
datetime.date.today() + datetime.timedelta(days=1) to set a sensible default departure date. Also used in strftime() calls inside each ticket presenter to format the date for display.PayPal, CreditCard, CashAtAirport, SimpleConsoleTicket, DetailedTicket, and Booking. It removes the need to write def __init__(self, ...): manually for any of these classes.PaymentMethod and TicketPresenter. Any class that provides the methods listed in these Protocols automatically satisfies them — no class PayPal(PaymentMethod) declaration needed.class PaymentMethod(Protocol): def name(self) -> str: ... def confirm_payment(self, amount: float, description: str) -> bool: ...
PaymentMethod is a structural interface. It declares exactly what any payment processor must be able to do, with no implementation details. The ... (Ellipsis) body is the conventional way to write a Protocol method — it signals that the method exists as a contract, not as runnable code.
The two required methods have precise contracts: name() must return a human-readable string identifying the payment method (used on the printed ticket), and confirm_payment() must accept an amount and description, interact with the user or an external service, and return True if payment was approved, False if not. The Booking class relies entirely on this contract — it never inspects the concrete type.
PaymentMethod not because it declares class MyClass(PaymentMethod), but because it has a name() and a confirm_payment() method with the right signatures. The Protocol simply documents and type-checks this expectation.
| Method | Return | Purpose |
|---|---|---|
| name() | str | Returns a display name for the payment method used in ticket output |
| confirm_payment(amount, description) | bool | Presents payment to user/service, returns True if approved |
class TicketPresenter(Protocol): def generate(self, booking: 'Booking') -> str: ...
TicketPresenter is the second structural interface. It defines only a single method: generate(), which accepts a Booking object and returns a formatted ticket string. The 'Booking' type hint is written as a string (a forward reference) because the Booking class is defined later in the file — Python resolves it lazily so no NameError occurs.
With this single-method Protocol, the Booking.confirm_and_issue() method can call self.presenter.generate(self) and receive a completely different formatted string depending on whether the presenter is a SimpleConsoleTicket or a DetailedTicket — all without any branching logic inside Booking itself.
PaymentMethod without being a ticket formatter, and vice versa. It also makes the system easier to extend: a new payment method does not require any changes to ticket-related code.
@dataclass class PayPal: def name(self) -> str: return "PayPal" def confirm_payment(self, amount: float, description: str) -> bool: print(f" → Opening PayPal → ${amount:,.2f} ({description})") print(" (simulated redirect to paypal.com ...)") ans = input(" Did you complete the payment? [yes/no]: ").strip().lower() return ans in ("y", "yes", "1", "true") @dataclass class CreditCard: def name(self) -> str: return "Credit Card" def confirm_payment(self, amount: float, description: str) -> bool: print(f" → Card terminal: ${amount:,.2f} ({description})") print(" (simulated 3D Secure / OTP screen ...)") ans = input(" Approve transaction? [yes/no]: ").strip().lower() return ans in ("y", "yes", "1", "true")
Both PayPal and CreditCard are decorated with @dataclass even though neither has any stored fields. The decorator is still valid and harmless here — it generates a no-argument __init__, which means both can be instantiated with just PayPal() and CreditCard(). More importantly, it signals to the reader that these are data-oriented objects following a consistent pattern with the rest of the program.
The polymorphism is clear here: PayPal.confirm_payment() simulates a browser redirect to paypal.com, while CreditCard.confirm_payment() simulates a 3D Secure OTP screen. Both accept the same arguments, both return a bool. From Booking's perspective they are interchangeable — the behaviour difference is entirely encapsulated inside each class.
confirm_payment() returning a bool is what lets confirm_and_issue() branch on success or failure. The truthy check ans in ("y", "yes", "1", "true") is deliberately generous — it accepts several common affirmative inputs rather than a strict equality check on a single string.
@dataclass class CashAtAirport: def name(self) -> str: return "Cash at Airport" def confirm_payment(self, amount: float, description: str) -> bool: print(f" → Payment option: Pay ${amount:,.2f} cash at airport counter") print(f" Description: {description}") ans = input(" Will you pay cash at airport? [yes/no]: ").strip().lower() return ans in ("y", "yes", "1", "true")
CashAtAirport is the third concrete implementation of PaymentMethod. Unlike PayPal and CreditCard, it does not simulate any electronic gateway — it displays the amount and description to the traveller and asks for a manual confirmation that they intend to pay at the counter. This is a meaningful difference in real-world behaviour, yet from Booking's perspective it is completely invisible. This is the power of the Protocol: three radically different payment flows share a single interface.
@dataclass class SimpleConsoleTicket: def generate(self, booking: 'Booking') -> str: dt = booking.departure_date.strftime("%b %d, %Y") return f""" ╔══════════════════════════════════════════════╗ ║ BOOKING CONFIRMED ║ ╚══════════════════════════════════════════════╝ Ticket : {booking.ticket_id} Passenger : {booking.name} Route : {booking.origin} → {booking.destination} Date : {dt} Passengers : {booking.passengers} Total : ${booking.total:,.2f} Paid via : {booking.payment.name()} """
SimpleConsoleTicket satisfies the TicketPresenter Protocol by implementing generate(). It uses a multiline f-string with Unicode box-drawing characters to produce a compact, visually distinct confirmation receipt. The date is formatted with strftime("%b %d, %Y") to produce output like Mar 17, 2026.
Notice that this class accesses booking.payment.name() — it calls the PaymentMethod Protocol through the Booking object. This means both Protocols cooperate at ticket-generation time: the presenter needs the payment name for the receipt, and the payment object provides it polymorphically. The class does not care whether payment is PayPal, CreditCard, or Cash — it calls .name() and gets the right string.
@dataclass class DetailedTicket: def generate(self, booking: 'Booking') -> str: dt = booking.departure_date.strftime("%A, %B %d, %Y %I:%M %p") return f""" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ E-TICKET • {booking.ticket_id} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Passenger name : {booking.name} Contact : {booking.email} Flight : {booking.origin} → {booking.destination} Travel date : {dt} Number of passengers : {booking.passengers} Base fare : ${booking.base_fare:,.2f} Total amount : ${booking.total:,.2f} Payment method : {booking.payment.name()} Status : CONFIRMED Thank you for choosing our airline. Safe travels! ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """
DetailedTicket also satisfies TicketPresenter through its generate() implementation. It displays significantly more information: the passenger's email, the full weekday-and-time date (strftime("%A, %B %d, %Y %I:%M %p") produces output like Tuesday, March 17, 2026 12:00 AM), and the base_fare broken out separately from the total.
| Field | SimpleConsoleTicket | DetailedTicket |
|---|---|---|
| Ticket ID | ✅ | ✅ |
| Passenger name | ✅ | ✅ |
| Email contact | ❌ | ✅ |
| Route | ✅ | ✅ |
| Date format | Mar 17, 2026 | Tuesday, March 17, 2026 12:00 AM |
| Base fare | ❌ | ✅ |
| Total | ✅ | ✅ |
| Payment method | ✅ | ✅ |
| Status line | ❌ | CONFIRMED |
@dataclass class Booking: name: str email: str origin: str destination: str passengers: int payment: PaymentMethod presenter: TicketPresenter ticket_id: str = "" total: float = 0.0 base_fare: float = 0.0 departure_date: datetime.date = datetime.date.today() + datetime.timedelta(days=1) def __post_init__(self): self.ticket_id = f"TK-{abs(hash(self.name + self.origin)) % 1000000:06d}" self.base_fare = self._calculate_base_fare() self.total = self.base_fare * self.passengers def _calculate_base_fare(self) -> float: routes = { ("Manila", "Cebu"): 78.0, ("Manila", "Davao"): 105.0, ("Manila", "Singapore"): 295.0, ("Manila", "Tokyo"): 540.0, ("Zamboanga", "Manila"): 92.0, } key = (self.origin.title(), self.destination.title()) return routes.get(key, 180.0)
The Booking dataclass is the core of the program. It holds all passenger and booking data as annotated fields. Fields without default values — name, email, origin, destination, passengers, payment, and presenter — are required arguments to the generated __init__. Fields with defaults — ticket_id, total, base_fare, and departure_date — do not need to be passed; they are either computed in __post_init__ or use sensible defaults.
__post_init__ is called automatically by @dataclass immediately after __init__ has set all fields. This is the right place to compute derived values that depend on constructor inputs. Here it generates a deterministic ticket ID from a hash of the passenger name and origin city, looks up the base fare from the routes dictionary, and multiplies it by the passenger count to set the total.
abs(hash(self.name + self.origin)) % 1000000 produces a deterministic six-digit number from the name and origin string. The same inputs always produce the same ticket ID, which makes the system predictable in testing. The :06d format specifier zero-pads the number to exactly six digits, producing IDs like TK-042718.
| Field | Type | Source |
|---|---|---|
| name | str | Required — passed from main() |
| str | Required — passed from main() | |
| origin / destination | str | Required — user input, normalized with .title() |
| passengers | int | Required — validated 1–9 in main() |
| payment | PaymentMethod | Required — polymorphic object from main() |
| presenter | TicketPresenter | Required — polymorphic object from main() |
| ticket_id | str | Derived in __post_init__ via hash |
| base_fare | float | Derived in __post_init__ via route lookup |
| total | float | base_fare × passengers in __post_init__ |
| departure_date | date | Default: today + 1 day |
def confirm_and_issue(self) -> None: description = f"{self.origin} → {self.destination} • {self.passengers} pax" print(f"\nTotal to pay: ${self.total:,.2f}") print(f"Using: {self.payment.name()}\n") if self.payment.confirm_payment(self.total, description): print("\n" + self.presenter.generate(self)) print("→ E-ticket sent to", self.email) else: print("\nBooking not completed – payment was not confirmed.")
This method is where polymorphism pays off completely. The entire payment and ticket-generation logic is four lines of meaningful code — no isinstance checks, no if payment_type == "paypal" branches. The method simply tells self.payment to confirm the payment and tells self.presenter to generate the ticket. Python handles the dispatch to the right concrete implementation automatically.
description to pass into the payment confirmation for display in the payment terminal simulation.self.payment.name() dispatches polymorphically — it returns "PayPal", "Credit Card", or "Cash at Airport" depending on the object.self.payment.confirm_payment(self.total, description) dispatches to the correct implementation. Each class shows its own UI (redirect, OTP screen, or cash instructions) and returns a bool.self.presenter.generate(self) dispatches to the correct ticket formatter. The result — either a compact receipt or a full e-ticket — is printed immediately.def main(): print("\n" + "═"*60) print(" POLYMORPHISM-FOCUSED TICKETING DEMO") print("═"*60) name = input("Full name : ").strip() or "Guest" email = input("Email : ").strip() orig = input("From : ").strip() dest = input("To : ").strip() while True: try: pax = int(input("Passengers (1–9) : ")) if 1 <= pax <= 9: break except: pass print("Enter number 1–9 please.") ch = input("Choose payment [1=PayPal 2=Card 3=Cash] → ").strip() if ch == "1": payment = PayPal() elif ch == "2": payment = CreditCard() else: payment = CashAtAirport() style = input("Ticket style [1=Simple 2=Detailed] → ").strip() presenter = SimpleConsoleTicket() if style != "2" else DetailedTicket() booking = Booking( name=name, email=email, origin=orig, destination=dest, passengers=pax, payment=payment, presenter=presenter, ) booking.confirm_and_issue()
main() is the entry point that wires everything together. Its job is to collect user inputs, instantiate the correct polymorphic objects, and hand them off to Booking. Notice that after the payment and presenter objects are created, main() never references their concrete types again — it passes them as abstract Protocol-satisfying objects to Booking. All further behaviour is delegated through polymorphism.
while True / try / except loop is a classic Python validation pattern. Wrapping int(input(...)) in a try block catches a ValueError if the user enters non-numeric text (e.g., "two"). The range check 1 <= pax <= 9 rejects out-of-range integers. The loop repeats until a valid integer is entered, at which point break exits.
if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n\nCancelled.") print("\nThank you! ✈️")
The if __name__ == "__main__" guard is a Python idiom that prevents main() from running when the file is imported as a module into another script. When Python runs a file directly, it sets __name__ to the string "__main__". When the same file is imported, __name__ becomes the module's file name instead, so the condition is False and main() is never called.
The try/except KeyboardInterrupt wrapper gracefully handles the user pressing Ctrl+C mid-booking. Without it, Python would print an ugly traceback. With it, the program prints Cancelled. and then the thank-you message, leaving the terminal in a clean state. The thank-you line runs regardless of whether the booking was completed, cancelled, or interrupted.
Polymorphism in Ticketing.py operates at two independent axes simultaneously: the payment axis (3 implementations of PaymentMethod) and the presentation axis (2 implementations of TicketPresenter). Because both axes are independent, the program supports 3 × 2 = 6 distinct booking flows — all handled by exactly the same confirm_and_issue() code.
GCash), simply write a new class with name() and confirm_payment(). To add a third ticket style (e.g., PDFTicket), simply write a class with generate(). Neither Booking nor any existing class needs to be changed. This is the open/closed principle in practice, enabled by polymorphism.
To understand why polymorphism matters, consider what confirm_and_issue() would look like without it. Every new payment method or ticket style would require modifying this core method — violating the open/closed principle and making the method increasingly complex and fragile with every addition.
# BAD — Must be modified every time a new payment or ticket type is added def confirm_and_issue(self): if isinstance(self.payment, PayPal): # PayPal-specific logic paid = paypal_redirect(self.total) elif isinstance(self.payment, CreditCard): # CreditCard-specific logic paid = card_terminal(self.total) elif isinstance(self.payment, CashAtAirport): # Cash-specific logic paid = cash_counter(self.total) # Adding GCash? Must add another elif here. Fragile. if paid: if isinstance(self.presenter, SimpleConsoleTicket): print(simple_format(self)) elif isinstance(self.presenter, DetailedTicket): print(detailed_format(self)) # Adding PDFTicket? Must add another elif here too.
# GOOD — Never needs to be modified when new types are added def confirm_and_issue(self): if self.payment.confirm_payment(self.total, description): print(self.presenter.generate(self)) else: print("Payment not confirmed.")
payment and presenter fields in Booking are typed as their Protocols, not their concrete classes. This is the fundamental OOP principle: depend on abstractions, not concrete implementations.Booking orchestrates; payment classes confirm payments; presenter classes format tickets. No class does more than its role demands.GCash, PDFTicket) and closed for modification (no existing class needs changing). This is achieved entirely through polymorphism._calculate_base_fare() method is private (prefixed with _). Fare logic is encapsulated inside Booking and is not accessible from outside. This protects the internal calculation from external interference.routes.get(key, 180.0) — if the entered route is not in the dictionary, a default fare of $180.00 is used rather than raising a KeyError. This makes the program robust to unfamiliar routes.while True / try / except loop pattern for the passenger count is reusable. The same pattern should be applied to any future numeric input (e.g., luggage count or seat selection) to keep validation consistent.The complete, runnable source code for Ticketing.py. Copy it, save it as ticketing.py, and run it with python ticketing.py.
import datetime from dataclasses import dataclass from typing import Protocol class PaymentMethod(Protocol): def name(self) -> str: ... def confirm_payment(self, amount: float, description: str) -> bool: ... class TicketPresenter(Protocol): def generate(self, booking: 'Booking') -> str: ... @dataclass class PayPal: def name(self) -> str: return "PayPal" def confirm_payment(self, amount: float, description: str) -> bool: print(f" → Opening PayPal → ${amount:,.2f} ({description})") print(" (simulated redirect to paypal.com ...)") ans = input(" Did you complete the payment? [yes/no]: ").strip().lower() return ans in ("y", "yes", "1", "true") @dataclass class CreditCard: def name(self) -> str: return "Credit Card" def confirm_payment(self, amount: float, description: str) -> bool: print(f" → Card terminal: ${amount:,.2f} ({description})") print(" (simulated 3D Secure / OTP screen ...)") ans = input(" Approve transaction? [yes/no]: ").strip().lower() return ans in ("y", "yes", "1", "true") @dataclass class CashAtAirport: def name(self) -> str: return "Cash at Airport" def confirm_payment(self, amount: float, description: str) -> bool: print(f" → Payment option: Pay ${amount:,.2f} cash at airport counter") print(f" Description: {description}") ans = input(" Will you pay cash at airport? [yes/no]: ").strip().lower() return ans in ("y", "yes", "1", "true") @dataclass class SimpleConsoleTicket: def generate(self, booking: 'Booking') -> str: dt = booking.departure_date.strftime("%b %d, %Y") return f""" ╔══════════════════════════════════════════════╗ ║ BOOKING CONFIRMED ║ ╚══════════════════════════════════════════════╝ Ticket : {booking.ticket_id} Passenger : {booking.name} Route : {booking.origin} → {booking.destination} Date : {dt} Passengers : {booking.passengers} Total : ${booking.total:,.2f} Paid via : {booking.payment.name()} """ @dataclass class DetailedTicket: def generate(self, booking: 'Booking') -> str: dt = booking.departure_date.strftime("%A, %B %d, %Y %I:%M %p") return f""" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ E-TICKET • {booking.ticket_id} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Passenger name : {booking.name} Contact : {booking.email} Flight : {booking.origin} → {booking.destination} Travel date : {dt} Number of passengers : {booking.passengers} Base fare : ${booking.base_fare:,.2f} Total amount : ${booking.total:,.2f} Payment method : {booking.payment.name()} Status : CONFIRMED Thank you for choosing our airline. Safe travels! ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """ @dataclass class Booking: name: str email: str origin: str destination: str passengers: int payment: PaymentMethod presenter: TicketPresenter ticket_id: str = "" total: float = 0.0 base_fare: float = 0.0 departure_date: datetime.date = datetime.date.today() + datetime.timedelta(days=1) def __post_init__(self): self.ticket_id = f"TK-{abs(hash(self.name + self.origin)) % 1000000:06d}" self.base_fare = self._calculate_base_fare() self.total = self.base_fare * self.passengers def _calculate_base_fare(self) -> float: routes = { ("Manila", "Cebu"): 78.0, ("Manila", "Davao"): 105.0, ("Manila", "Singapore"): 295.0, ("Manila", "Tokyo"): 540.0, ("Zamboanga", "Manila"): 92.0, } key = (self.origin.title(), self.destination.title()) return routes.get(key, 180.0) def confirm_and_issue(self) -> None: description = f"{self.origin} → {self.destination} • {self.passengers} pax" print(f"\nTotal to pay: ${self.total:,.2f}") print(f"Using: {self.payment.name()}\n") if self.payment.confirm_payment(self.total, description): print("\n" + self.presenter.generate(self)) print("→ E-ticket sent to", self.email) else: print("\nBooking not completed – payment was not confirmed.") def main(): print("\n" + "═"*60) print(" POLYMORPHISM-FOCUSED TICKETING DEMO") print("═"*60) name = input("Full name : ").strip() or "Guest" email = input("Email : ").strip() orig = input("From : ").strip() dest = input("To : ").strip() while True: try: pax = int(input("Passengers (1–9) : ")) if 1 <= pax <= 9: break except: pass print("Enter number 1–9 please.") print("\nPayment method:") print(" 1 = PayPal") print(" 2 = Credit Card") print(" 3 = Cash at Airport") ch = input("Choose → ").strip() if ch == "1": payment = PayPal() elif ch == "2": payment = CreditCard() else: payment = CashAtAirport() print("\nTicket style:") print(" 1 = Simple") print(" 2 = Detailed") style = input("Choose → ").strip() presenter = SimpleConsoleTicket() if style != "2" else DetailedTicket() booking = Booking( name=name, email=email, origin=orig, destination=dest, passengers=pax, payment=payment, presenter=presenter, ) booking.confirm_and_issue() if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n\nCancelled.") print("\nThank you! ✈️")
self.payment.confirm_payment() behaves differently for PayPal, CreditCard, and CashAtAirport — but Booking never needs to know which one it is dealing with. Python dispatches to the correct implementation automatically at runtime.class MyClass(MyABC) and will raise a TypeError at instantiation if abstract methods are not implemented. A Protocol uses structural subtyping — any class that implements the required methods automatically satisfies the Protocol, with no inheritance declaration needed. Protocols are checked by static type checkers like mypy at development time, not enforced by Python at runtime.@dataclass generates a zero-argument __init__ and a __repr__ that prints the class name. It is harmless and consistent — it signals that this class is a data-oriented object following the same pattern as Booking. It also means all classes can be instantiated with the same syntax: PayPal(), CreditCard(), SimpleConsoleTicket(), and so on.__post_init__ is a special method recognised by @dataclass. It is called automatically immediately after the generated __init__ has assigned all annotated fields. This makes it the correct place to compute derived values that depend on constructor inputs. In Booking, __post_init__ generates the ticket ID from the name and origin, looks up the base fare from the routes dictionary, and calculates the total — all values that require the constructor fields to be set first.'Booking' instead of Booking. It is needed when the annotated type has not yet been defined at the point the annotation is parsed. In TicketPresenter.generate(self, booking: 'Booking'), Booking is defined later in the file, so the string form tells Python to resolve it lazily at type-checking time rather than immediately, preventing a NameError.name() -> str and confirm_payment(amount, description) -> bool automatically satisfies the PaymentMethod Protocol. Simply instantiate it and pass it to Booking. The confirm_and_issue() method never needs to be touched. The same principle applies to ticket presenters — implement generate(booking) -> str and it works immediately.Now that you understand how polymorphism and Protocols work in Ticketing.py, try adding a GCash payment class, a MobileTicket presenter with emoji formatting, or a LoyaltyDiscount mixin that reduces the base fare. Each extension requires zero changes to Booking — that is the power of programming to interfaces.
Stay connected for more Python guides, OOP tutorials, and tech content from the Philippines. Follow on your preferred platform:
Get new Python tutorials, OOP guides, and tech articles from Valleys & Bytes delivered to your inbox — no spam, ever.
🔒 No spam. Unsubscribe any time.
💬 Leave a Comment
💡 Comments are stored locally in this session. Be respectful and constructive.