Course contents

Polymorphism and abstract base classes

Sign in to run this lab and save your progress

Learn

Before you start

This is the last lesson of the OOP foundations module. It puts a name on something you've already been doing, and gives you a bigger toolkit for the rest of the course.

What you already know without knowing it

Polymorphism is not a new idea. You've been using it:

  • Lesson 3. A list of Vehicle subclasses, each with its own describe(). The loop for v in fleet: print(v.describe()) doesn't care which subclass each v is. That's polymorphism.
  • Lesson 4. A list of notification channels. Same thing.

The formal word is polymorphism. Greek roots: poly (many)

  • morph (shape). Many shapes, one way to use them.

Two ways to spell the rule

Python gives you two choices:

Duck typing — the loose default. If it walks like a duck. No declaration needed. Just call the methods and hope.

Abstract base class (ABC) — the formal contract. Says "every child class MUST have these methods". If a child is missing one, Python refuses to build it.

Real production Python uses both. Rough guide:

  • Two or three methods, all your own code → duck typing
  • Public interface, many child classes possible → ABC

For interviews, using ABC shows design maturity — you're naming the rule instead of leaving it implicit. Interviewers notice.

Why "abstract" is a library, not a keyword

In Java or C#, abstract is built into the language. In Python, it's a library — abc.ABC + @abstractmethod. The effect is the same:

  1. You can't build the parent class directly.
  2. Child classes that don't have all the required methods ALSO can't be built.
  3. Child classes that have every required method can be built freely.

This is Python's style: don't invent language features when a library will do.

The pattern preview

Almost every design pattern in module 3 uses this shape. Strategy? A base with abstract methods and concrete strategies. Observer? Same. Command? Same. State? Same.

Get comfortable with the ABC-plus-child-classes shape today. You'll see it everywhere from now on.

Payment methods as the running example

Payments are the classic example for a reason: every real e-commerce system has 5-10 payment types, and every one has the same three-verb shape (charge, refund, sometimes pre-authorize). The alternative — a giant if method == "credit_card": ... elif method == "upi": ... — is exactly what the open-closed principle (next module) tells you to avoid.

Build this correctly today and the design pattern module will feel like a natural next step.

Your task

Polymorphism means: many types, one shared way to use them. Your code uses the shared methods, and any class that has those methods can be plugged in.

You've already been doing polymorphism without knowing the word:

  • Lesson 3 — a mixed list of Vehicle subclasses, all with a .describe() method.
  • Lesson 4 — a list of notification channels, all with a .send() method.

This lesson gives you two ways to make the "shared methods" rule official: duck typing and abstract base classes.

Duck typing (Python's default)

Python doesn't need you to declare which methods a class should have. If an object has the methods you call, it works. Named after the old saying: "if it walks like a duck and quacks like a duck, it's a duck".

def send_all(channels, msg):
    for c in channels:
        c.send(msg)      # any object with a .send() method is fine

Simple. Loose. Good for small internal code (lesson 4 used this).

Downside: if you pass an object that's MISSING the method, nothing warns you. You find out at runtime, when your code crashes.

Abstract base classes (formal contracts)

When you want a real rule — "every payment method MUST have charge" — use abc.ABC:

from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def charge(self, amount):
        ...

    @abstractmethod
    def refund(self, transaction_id):
        ...

Now:

  • PaymentMethod() raises an error. You can't build an abstract class directly.
  • A subclass that forgets to write charge or refund ALSO raises an error when you try to build it.
  • Every subclass is guaranteed to have those methods. The caller can trust it.
class CreditCard(PaymentMethod):
    def charge(self, amount):
        return f"charged {amount} to credit card"
    def refund(self, transaction_id):
        return f"refunded {transaction_id} to credit card"

When to use which

Duck typing when:

  • You only need one or two methods.
  • All the classes are in your own code.
  • You're OK finding missing methods at runtime.

ABC when:

  • Other people will write classes that follow your rule.
  • You want missing methods to fail LOUDLY at build time, not later.
  • You want one place to see all the required methods.

Both are polymorphism. Both are fine. Interviewers care that you can EXPLAIN why you picked one.

Your task

You're building a payment system for an e-commerce checkout. Multiple payment types, one shared interface.

  1. Abstract base class PaymentMethod with two abstract methods:
    • charge(self, amount) returning a string
    • refund(self, transaction_id) returning a string
  2. Three concrete subclasses:
    • CreditCardcharge returns "charged <amount> via credit card", refund returns "refunded <transaction_id> via credit card"
    • Upi"charged <amount> via UPI" / "refunded <transaction_id> via UPI"
    • Wallet"charged <amount> via wallet" / "refunded <transaction_id> via wallet"
  3. A Checkout class that:
    • Takes a PaymentMethod in its constructor
    • Has pay(amount) that delegates to the payment method's charge and returns its result
    • Has undo(transaction_id) that delegates to refund

The tests verify each concrete method AND that trying to instantiate PaymentMethod directly raises TypeError. Click Submit to run them.

Starter files

Preview only — open the lab to edit and run.

files_dir
05-polymorphism-and-abc-starter