Skip to Content
Course content

121: Polymorphism in Practice

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably heard the academic definition of polymorphism—something about "many forms"—but in the trenches of a real codebase, it's less about theory and more about how you handle growth. When I see junior devs struggling with a codebase that's becoming a "spaghetti monster," it's almost always because they're fighting against polymorphism instead of using it.

The "Type-Checking" Trap

Let's say we're building a payment processing system. You've got Credit Cards, PayPal accounts, and maybe some crypto wallets. The naive way to handle this is to create a central "manager" function that checks what it's dealing with before deciding what to do. It usually looks something like this:

def process_payment(payment_method, amount):
    if isinstance(payment_method, CreditCard):
        payment_method.charge_card(amount)
    elif isinstance(payment_method, PayPal):
        payment_method.send_paypal_payment(amount)
    elif isinstance(payment_method, BitcoinWallet):
        payment_method.transfer_crypto(amount)
    else:
        raise ValueError("Unsupported payment method")

At first glance, this feels safe. You know exactly which method is being called for which class. But here is where the technical debt starts accruing. Every time the business decides to add a new payment method—say, Apple Pay or Klarna—you have to go back into this specific function and add another elif block. You're modifying existing, working code to add new functionality, which is a great way to accidentally break your checkout flow on a Friday afternoon.

Letting the Objects Do the Work

The better way—the polymorphic way—is to stop asking the object what it is and start telling it what to do. We agree on a common interface. I don't care if it's a credit card or a magic bean; if it's a "Payment Method," it must have a method called pay().

class CreditCard:
    def pay(self, amount):
        print(f"Charging ${amount} to Credit Card")

class PayPal:
    def pay(self, amount):
        print(f"Redirecting to PayPal for ${amount}")

class BitcoinWallet:
    def pay(self, amount):
        print(f"Transferring {amount} BTC from wallet")

def process_payment(payment_method, amount):
    # This function never needs to change again
    payment_method.pay(amount)

Now, the process_payment function is blissfully ignorant. It doesn't know the classes exist; it just knows that whatever object it receives has a pay method. This is "Duck Typing" in its purest form: if it walks like a duck and quacks like a duck, Python treats it as a duck.

The Trade-off: Implicit vs. Explicit

You might be thinking, "But now I don't have that ValueError for unsupported types!" That's a fair point. By moving to polymorphism, you've traded a compile-time-ish check (the if/else) for a potential runtime AttributeError if you pass in an object that doesn't have a pay method.

In a small script, that's fine. In a massive enterprise system, I'd suggest using an Abstract Base Class (ABC) to enforce the contract. By inheriting from an ABC, Python will actually prevent you from even instantiating a payment class if you forgot to implement the pay method. It gives you the architectural cleanliness of polymorphism with the safety net of an explicit requirement. But the core philosophy remains: stop checking types and start trusting your interfaces.




📋 Practical Task

Refactoring the Multi-Channel Notification Dispatcher

You've inherited a piece of code that handles sending notifications to users. Currently, it's written using the "type-checking trap" we discussed. Your goal is to refactor this into a polymorphic system so that adding new notification channels (like Slack or Discord) doesn't require changing the dispatcher logic.

The Current Mess:

class EmailService:
    def send_email(self, message):
        print(f"Sending Email: {message}")

class SMSService:
    def send_sms(self, message):
        print(f"Sending SMS: {message}")

def notify_user(service, message):
    if isinstance(service, EmailService):
        service.send_email(message)
    elif isinstance(service, SMSService):
        service.send_sms(message)

Your Task:

  • Modify EmailService and SMSService so they share a common method name for sending messages.
  • Rewrite the notify_user function so it no longer uses isinstance or if/elif blocks.
  • Create a new class called PushNotificationService with its own send method and verify that notify_user can handle it without any modifications to the function's internal code.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.