ADVERTISEMENT
Python · Polymorphism · Protocol · OOP

Ticketing.py

Python OOP · Polymorphism Guide

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.

✈️ Airline Ticketing 🔌 2 Protocols 💳 3 Payment Methods 🎫 2 Ticket Styles 🔁 12 Sections 🐍 Python 3
SCROLL TO EXPLORE
INTRODUCTION

What Is Ticketing.py and Why Does It Matter?

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.

🐍
What you will learn from this guide: Defining structural interfaces with 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.
💡
Who this guide is for: Python learners who understand basic classes and want to understand how polymorphism and Protocols work in modern Python. No external libraries are needed — only datetime, dataclasses, and typing from the standard library are used throughout.
ADVERTISEMENT
SECTION 01

Program Overview

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.

Protocol 1 — PaymentMethod
Defines the interface any payment class must satisfy: name() → str and confirm_payment(amount, description) → bool. No inheritance required — any class with these methods qualifies.
Protocol 2 — TicketPresenter
Defines the interface any ticket formatter must satisfy: generate(booking) → str. Allows the Booking class to generate tickets without knowing whether the format is simple or detailed.
PayPal / CreditCard / Cash
Three concrete classes that each satisfy the PaymentMethod Protocol by implementing both required methods with different logic: redirect URL, 3D Secure simulation, and cash counter instructions.
SimpleConsoleTicket / DetailedTicket
Two concrete classes satisfying TicketPresenter. One returns a compact ASCII-art receipt; the other returns a full e-ticket with contact, full date, and base fare breakdown.
Booking — The Dataclass
Holds all passenger and booking data. Uses __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().
🖥️Input
main() collects name, email, route, passengers
💳Payment
User picks PayPal / Card / Cash — polymorphic object created
🎫Style
User picks Simple or Detailed — polymorphic presenter created
📦Booking
Dataclass auto-calculates fare & ticket ID
Issue
confirm_and_issue() dispatches polymorphically — no if/elif
SECTION 02

import datetime, dataclasses, typing — The First Lines

Ticketing.py — Lines 1–3
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 module
Used as 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.
dataclass decorator
Applied to PayPal, CreditCard, CashAtAirport, SimpleConsoleTicket, DetailedTicket, and Booking. It removes the need to write def __init__(self, ...): manually for any of these classes.
Protocol class
Used to define PaymentMethod and TicketPresenter. Any class that provides the methods listed in these Protocols automatically satisfies them — no class PayPal(PaymentMethod) declaration needed.
SECTION 03

The PaymentMethod Protocol

class PaymentMethod(Protocol)
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.

🦆
Duck typing in action: Python's philosophy is "if it walks like a duck and quacks like a duck, it's a duck." A class satisfies 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.
MethodReturnPurpose
name()strReturns a display name for the payment method used in ticket output
confirm_payment(amount, description)boolPresents payment to user/service, returns True if approved
SECTION 04

The TicketPresenter Protocol

class TicketPresenter(Protocol)
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.

📐
Why separate Protocols for payment and presentation? Each Protocol follows the Single Responsibility Principle — it captures one and only one capability. This means a class can satisfy 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.
SECTION 05

PayPal & CreditCard — Payment Implementations

class PayPal / class CreditCard
@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.

💡
The return value matters: 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.
SECTION 06

CashAtAirport — Third Payment Option

class CashAtAirport
@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.

PayPal
Simulates browser redirect. Prints paypal.com message. User confirms redirect completion.
CreditCard
Simulates 3D Secure OTP. Prints terminal amount. User approves or declines transaction.
CashAtAirport
No gateway. Shows counter instructions. User confirms intent to pay cash at the airport.
SECTION 07

SimpleConsoleTicket — Compact Ticket Format

class SimpleConsoleTicket
@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.

SECTION 08

DetailedTicket — Full E-Ticket Format

class DetailedTicket
@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.

SimpleConsoleTicket vs. DetailedTicket — Side-by-Side
FieldSimpleConsoleTicketDetailedTicket
Ticket ID
Passenger name
Email contact
Route
Date formatMar 17, 2026Tuesday, March 17, 2026 12:00 AM
Base fare
Total
Payment method
Status lineCONFIRMED
SECTION 09

The Booking Dataclass

@dataclass class Booking — Fields & __post_init__
@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__ — Running Logic After Field Assignment

__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.

🔑
Why hash for the ticket ID? 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.
FieldTypeSource
namestrRequired — passed from main()
emailstrRequired — passed from main()
origin / destinationstrRequired — user input, normalized with .title()
passengersintRequired — validated 1–9 in main()
paymentPaymentMethodRequired — polymorphic object from main()
presenterTicketPresenterRequired — polymorphic object from main()
ticket_idstrDerived in __post_init__ via hash
base_farefloatDerived in __post_init__ via route lookup
totalfloatbase_fare × passengers in __post_init__
departure_datedateDefault: today + 1 day
SECTION 10

confirm_and_issue() — Polymorphism at Work

Booking.confirm_and_issue()
    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.

1
Build description string
A human-readable route and passenger count summary is assembled as description to pass into the payment confirmation for display in the payment terminal simulation.
2
Print total and payment method
The user is shown the total amount and which payment method is being used. self.payment.name() dispatches polymorphically — it returns "PayPal", "Credit Card", or "Cash at Airport" depending on the object.
3
confirm_payment() dispatch
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.
4
generate() dispatch — if payment approved
If the bool is truthy, self.presenter.generate(self) dispatches to the correct ticket formatter. The result — either a compact receipt or a full e-ticket — is printed immediately.
SECTION 11

The main() Function — Input, Selection, Dispatch

def main()
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.

🔒
Input validation for passenger count: The 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.
SECTION 12

The __name__ Guard — Safe Entry Point

Ticketing.py — Final Lines
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.

DEEP DIVE

Polymorphism — The Full Picture

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.

💳PayPal
→ confirms via redirect simulation
💳CreditCard
→ confirms via OTP simulation
💵CashAtAirport
→ confirms via intent declaration
🎫SimpleTicket
→ compact ASCII box receipt
📄DetailedTicket
→ full e-ticket with all fields
🔓
Open/Closed Principle: Ticketing.py is open for extension, closed for modification. To add a fourth payment method (e.g., 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.
Without Polymorphism — The Alternative

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.

❌ Without Polymorphism — Fragile, Unscalable
# 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.
✅ With Polymorphism — Clean, Extensible
# 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.")
BEST PRACTICES

Design Principles in Ticketing.py

Program to Interfaces
Both 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.
Single Responsibility
Each Protocol captures one capability. Each dataclass does one job. Booking orchestrates; payment classes confirm payments; presenter classes format tickets. No class does more than its role demands.
Open/Closed
The program is open for extension (add GCash, PDFTicket) and closed for modification (no existing class needs changing). This is achieved entirely through polymorphism.
Encapsulation
The _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.
Defensive Defaults
The route lookup uses 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.
Consistent Validation
The 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.
FULL SOURCE

Complete Ticketing.py Source Code

The complete, runnable source code for Ticketing.py. Copy it, save it as ticketing.py, and run it with python ticketing.py.

ticketing.py — Full Source
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! ✈️")
Was this helpful?
SHARE

Share This Guide

✨ AI-Powered Share Caption

Generate a ready-to-post social media caption for this guide. Pick your platform and tone, then click Generate.

Generating caption…
FAQ

Frequently Asked Questions

Polymorphism means "many forms." In Python, it describes the ability of a single method call to produce different behaviour depending on the type of the object it is called on. In Ticketing.py, 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.
An abstract base class (ABC) requires explicit inheritance — a subclass must declare 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.
Applied to a class with no annotated fields, @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.
A forward reference is a type annotation written as a string rather than a direct class reference — for example, '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.
Yes — this is the open/closed principle enabled by polymorphism. Any new class that implements 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.
✈️ Ready to Extend Ticketing.py?

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.

COMMUNITY

Comments & Discussion

💬 Leave a Comment

💡 Comments are stored locally in this session. Be respectful and constructive.

R
Rico Santos
March 2026
The side-by-side code comparison showing the polymorphic vs. non-polymorphic version of confirm_and_issue() finally made the whole concept click. I always knew isinstance checks were bad practice, but now I understand exactly why and how Protocol solves it cleanly.
L
Lena Cruz
March 2026
I never realized @dataclass and Protocol together are such a powerful combination. The __post_init__ section especially — I had no idea that was how you were supposed to compute derived fields. Using this as a reference for my own booking system at work.
J
Jun Reyes
March 2026
The 3×2 = 6 distinct booking flows point in the polymorphism deep dive section is brilliant. I never thought about it that way — two independent Protocol axes multiplying the number of supported combinations without any additional code in Booking. That is elegant design.
CONNECT

Follow Glenn Junsay Pansensoy

Stay connected for more Python guides, OOP tutorials, and tech content from the Philippines. Follow on your preferred platform:

📘 🐦 💼