Skip to content

Interfaces and polymorphism: one call, many behaviours

How to replace a chain of if-statements on a kind with an interface, so that a new kind is a new class and the calling code does not change.

  • Lesson 11 of 169
  • 21 min read
  • Core
  • Free to read · No account needed
On this page
  1. Why interviewers ask this
  2. The idea in plain words
  3. Pause and predict
  4. Worked example
  5. Trade-offs
  6. Common misconceptions
  7. How to say it in the interview
  8. Check your understanding
  9. Answers
  10. Summary card: interfaces and polymorphism
  11. Sources

Before you start

This lesson uses ideas from these earlier lessons. Read them first if any idea is new to you.

01Why interviewers ask this

In a coding round the interviewer adds one new requirement after your first version works. Very often it is "one more kind of something": one more way to pay, one more vehicle, one more discount. Your answer to this twist shows your design.

A weak design has a chain of if kind == ... in several places. A new kind means an edit in each place, and it is easy to forget one. A strong design has one list of methods that every kind promises, and one class for each kind. A new kind is a new class, and the old code does not change. This is worth points in the extensibility part of the score (lesson 7): how small the change is when a new rule arrives.

At SDE-1 level you should write the list of promised methods and two or three classes that keep it. At SDE-2 level you should also say when such a list is not worth adding, and write a small stand-in class for a test.

02The idea in plain words

Think of the power sockets in your home. All sockets accept the same plug. A fan, a phone charger and a kettle all fit. The wall does not ask what is plugged in. It only needs the plug to fit. When you buy a new appliance, you do not open the wall.

In code, the socket is an interface. An interface is a list of methods that a class promises to have. Code that uses the interface does not care which class is behind it.

The problem: a chain of ifs

Our canteen (from the last two lessons) lets a customer pay a bill in three ways. The first is the canteen card. The second is UPI, a phone payment in which a bank moves the money. The third is cash. Here is the first draft. The text balance=0 means: if the caller gives no balance, use 0.

python
def checkout_old(kind, amount, balance=0, limit_left=0, cash=0):
    if kind == "card":
        return amount <= balance
    elif kind == "upi":
        return amount <= limit_left
    elif kind == "cash":
        return cash >= amount
    else:
        raise ValueError("unknown kind")

It works. The trouble is that the same chain soon appears again. It appears in the code that prints the receipt, and in the code that writes the refund text. That makes three chains in three functions. A fourth way to pay means three edits, and each edit can break working code.

The fix: an abstract class as the interface

In Python the clear way to write an interface is an abstract class: a class that lists methods but does not fill them in. You write it with ABC (short for abstract base class) and mark each unfilled method with @abstractmethod.

python
from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount):
        """Try to take amount (whole rupees, more than 0).
        Return True if paid. Return False if refused, and change nothing.
        Do not raise an error for a refusal."""

    def _valid(self, amount):
        return amount > 0

An abstract class cannot be made into an object. PaymentMethod() fails with a TypeError, which is Python's error name for a use that is not allowed here. It exists only to be filled in. A class that fills in every method is an implementing class. Python writes the link in brackets: class CardPayment(PaymentMethod): means "CardPayment implements PaymentMethod".

The text between the triple quotes is a docstring. Use it to write the contract: what goes in, what comes out, and which errors may happen. Every implementing class must keep that contract, because the calling code relies on it. Here the contract says a refusal is False, never an error.

An abstract class may also hold finished methods that every kind needs. _valid is one: it checks that the amount is more than 0. Every implementing class can call self._valid(amount) instead of repeating the check. Put in the abstract class only what is true for every kind.

Two implementing classes

CardPayment holds a canteen card, the class from the last lesson. It asks the card to pay. The card raises a ValueError when the balance is too low or the amount is wrong, so CardPayment catches it and returns False. This catches only ValueError; any other error still stops the program.

python
class CardPayment(PaymentMethod):
    def __init__(self, card):
        self._card = card

    def pay(self, amount):
        try:
            self._card.pay(amount)
        except ValueError:
            return False
        return True

UpiPayment keeps how much it may still pay today.

python
class UpiPayment(PaymentMethod):
    def __init__(self, limit_left):
        self._limit_left = limit_left

    def pay(self, amount):
        if not self._valid(amount) or amount > self._limit_left:
            return False
        self._limit_left -= amount
        return True

Polymorphism: one call, different work

The calling code takes the interface, not one class:

python
def checkout(amount, method):
    if method.pay(amount):
        return f"Rs {amount}: paid"
    return f"Rs {amount}: refused"

The text starting with f" is a text with {...} holes, as in lesson 9. checkout makes one call, method.pay(amount). What happens depends on the object that arrives. A card object takes money from a card. A UPI object checks the limit. Polymorphism means this: one call, different work, chosen by the object. The word means "many shapes".

A new kind is a new class and zero edited lines in checkout. checkout never learns the new class by name.

A lighter way: duck typing and Protocol

Python does not ask what class an object has. It only asks whether the object has the method you call. This is duck typing. The name comes from a saying: if it walks like a duck and quacks like a duck, treat it as a duck. Here: if the object has a pay method, treat it as a payment method. A class that is not linked to PaymentMethod at all, but has a method pay, works in checkout.

A type checker is a separate program (for example mypy) that reads your code without running it and reports calls that do not fit. Most rounds do not run one. A Protocol (from the typing library) writes the duck-typing idea down as a list of methods, and a type checker reads it. Python itself does not check a Protocol when the program runs.

python
from typing import Protocol

class Payer(Protocol):
    def pay(self, amount): ...

The three dots mean "no body here". In a round, prefer the abstract class. It is visible, and Python checks it for you. If a class forgets a method, you get a TypeError when you try to make an object of that class. Use a Protocol when you work with classes you cannot change.

Small interfaces, and when not to add one

Give an interface one or two methods. Name it by the job it does (PaymentMethod), not by its type (IPayment, BaseClass). A big interface forces every class to write methods it does not need.

If there is one class and the problem gives no sign of a second kind, do not add an interface. It costs a name and an extra jump for the reader. Add it when there are two real kinds, or when the interviewer says another kind is coming.

A chain of isinstance checks is still an if-chain

A chain of isinstance checks has the same problem as the chain of ifs. isinstance(x, Y) asks "is x an object of class Y?" If you see a chain like this, put the text in a method of the interface.

python
def refund_text_old(method):
    if isinstance(method, CardPayment):
        return "to card"
    elif isinstance(method, UpiPayment):
        return "to UPI"
    return "in cash"

A fake class for a test

A fake is a small class you write that stands in for a real one, so that you can try your code. It does what you tell it and records what it was asked.

python
class FakePayment(PaymentMethod):
    def __init__(self, will_pay):
        self.will_pay = will_pay
        self.asked = []

    def pay(self, amount):
        self.asked.append(amount)
        return self.will_pay

In Java

Java has the keywords. The interface lists methods, a class says implements, and an abstract class can hold finished methods. The word extends means that the class fills in an abstract class.

java
interface PaymentMethod { boolean pay(int amount); }
abstract class BaseMethod implements PaymentMethod {
    protected boolean valid(int amount) { return amount > 0; }
}
class UpiPayment extends BaseMethod {
    private int limitLeft = 100;
    public boolean pay(int amount) {
        if (!valid(amount) || amount > limitLeft) return false;
        limitLeft -= amount;
        return true;
    }
}

This box was compiled with Java 17, exactly as printed.

Want to check your understanding? Try explaining this idea in your own words. You can continue reading first, or ask the AI tutor a question. Sign up free to ask the tutor

03Pause and predict

Pause. Guess first.

The same if kind == ... chain appears in 3 functions of the canteen program. The canteen adds a fourth way to pay, a voucher. How many existing functions must you edit with the chain? And with the interface, how many existing lines must you edit?

Write your guess before you read on. The answer is in the worked example.

04Worked example

Assumption: a customer pays a canteen bill of Rs 140. The customer has three ways to pay. The first is a student card with Rs 200 on it (the CanteenCard and CardType from the last lesson). The second is UPI with Rs 100 of its limit left today, and the third is Rs 150 in cash. Run with Python 3.11. The classes above are in place. Cash needs one more class:

python
class CashPayment(PaymentMethod):
    def __init__(self, cash_given):
        self._cash_given = cash_given

    def pay(self, amount):
        return self._valid(amount) and self._cash_given >= amount

Now pay the same bill three ways.

python
card = CanteenCard("C101", CardType.STUDENT, 200)
print(checkout(140, CardPayment(card)), card.balance)
print(checkout(140, UpiPayment(100)))
print(checkout(140, CashPayment(150)))

It prints Rs 140: paid 60, then Rs 140: refused, then Rs 140: paid. The arithmetic: the card goes from 200 to 200 - 140 = Rs 60. The UPI limit is 100, and 140 is more than 100, so it is refused. The cash 150 is at least 140, so it is paid.

Now the twist: a voucher, a printed paper worth Rs 150 that the canteen accepts. It is one new class:

python
class VoucherPayment(PaymentMethod):
    def __init__(self, value):
        self._value = value

    def pay(self, amount):
        return self._valid(amount) and amount <= self._value

print(checkout(140, VoucherPayment(150)))

It prints Rs 140: paid, because 140 is not more than 150. checkout did not change, and CardPayment, UpiPayment and CashPayment did not change. Zero existing lines were edited. With the if-chain version, you would have edited the 3 functions that hold the chain. This answers the predict box: 3 functions against 0 lines.

The function checkout calls pay on a PaymentMethod. Four classes fill in PaymentMethod: CardPayment, UpiPayment…
A new kind is one new class, and the caller does not change.
The function checkout calls pay on a PaymentMethod. Four classes fill in PaymentMethod: CardPayment, UpiPayment, CashPayment and the new VoucherPayment. Checkout does not know the class names. Adding VoucherPayment changes no line in checkout and none in the three older classes.

Last, a check with the fake, with no real card or bank. An assert line stops the program with an error if its condition is false, and stays silent if it is true.

python
fake = FakePayment(True)
assert checkout(140, fake) == "Rs 140: paid"
assert fake.asked == [140]
print("checks passed")

It prints checks passed. The fake paid, and recorded that it was asked for 140. If a class forgets pay, for example class Incomplete(PaymentMethod) with an empty body, then Incomplete() fails with a TypeError. Python catches the missing method for you.

05Trade-offs

An interface solves the cost of a new kind: you add one class, and the callers do not change. It also makes tests easy, because a fake can take the place of a real one. It costs a name, one more class to read, and a jump when you read the code. The contract must also suit every kind; one kind that cannot keep it makes the interface wrong. I would change to a plain function when there is only one kind and none is expected. I would change to a simple data field when kinds differ only by a number.

06Common misconceptions

"Every class needs an interface." No. With one class and no sign of a second, an interface is extra reading for no gain. Add one when two kinds exist or are asked for.

"An isinstance chain is not an if-chain." It is the same chain. The new kind still needs an edit in every chain. Move the difference into a method of the interface.

"Python tells me at once when a child forgets a method." It tells you when you try to make an object, not when you write the class. A class that forgets pay is accepted until you call Incomplete().

07How to say it in the interview

Say it like this

When behaviour differs by kind, I write an interface with the methods every kind promises, and one class for each kind. The caller makes one call, so a new kind is a new class and no edit in old code.

08Check your understanding

  1. In one sentence, what is polymorphism? What happens when you write PaymentMethod() for an abstract class?
  2. A student card holds Rs 200. A customer pays a bill of Rs 140 and then a bill of Rs 90, both with CardPayment. What does checkout print each time, and what is the card balance at the end?
  3. A teammate adds a net-banking way as a new elif branch in checkout_old only. The refund text function still has its own chain and returns "in cash" for net banking. What is the flaw, and what do you do?

09Answers

Show the answers
  1. Polymorphism is one call that does different work, chosen by the object that receives it. PaymentMethod() fails with a TypeError, because an abstract class cannot be made into an object.
  2. The first bill: 140 is at most 200, so it prints Rs 140: paid and the balance is 200 - 140 = Rs 60. The second bill: 90 is more than 60, so CanteenCard.pay raises, CardPayment returns False, and it prints Rs 90: refused. The balance stays Rs 60.
  3. The same chain is in more than one place, and the teammate edited only one. Write NetBankingPayment as an implementing class. Move the refund text into a method of the interface, so each class says its own text, and delete the chains.

10Summary card: interfaces and polymorphism

One-page summary

  • Idea. An interface is a list of methods a class promises. Callers use the interface, so one call does different work for each kind.
  • Python. class X(ABC) with @abstractmethod. You cannot make an object of it. A class that forgets a method fails when you make it.
  • Contract. Write input, output and errors in the docstring. Every class must keep it.
  • New kind. One new class, zero edited lines in the caller.
  • Lighter way. Duck typing and Protocol are checked by a type checker, not by Python when the program runs.
  • Test. A fake class stands in for the real one.
  • Trade-off. It solves the cost of a new kind. It costs a name and a jump. Skip it for one kind.
  • Trap. An isinstance chain is the same if-chain.

11Sources

Try what you just learned

Put this idea into practice with a short exercise or a relevant tool. Free to try. No sign-up needed.

More practice. This needs a free account.

Continue learning with the lessons that build on this one.

Practise this idea

Have a question about this lesson?

Ask the AI tutor to explain a difficult idea, walk through an example or check your understanding. It uses the lesson to help you work through the topic.

A free account includes 10 AI tutor questions a day. The lesson remains free to read without an account.

All lessons in Module 2: Low-level design and machine coding

Prefer another topic? Browse all 169 lessons.