On this page
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.
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.
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 > 0An 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.
class CardPayment(PaymentMethod):
def __init__(self, card):
self._card = card
def pay(self, amount):
try:
self._card.pay(amount)
except ValueError:
return False
return TrueUpiPayment keeps how much it may still pay today.
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 TruePolymorphism: one call, different work
The calling code takes the interface, not one class:
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.
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.
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.
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_payIn 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.
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:
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 >= amountNow pay the same bill three ways.
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:
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.
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.
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
- In one sentence, what is polymorphism? What happens when you write
PaymentMethod()for an abstract class? - 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 doescheckoutprint each time, and what is the card balance at the end? - A teammate adds a net-banking way as a new
elifbranch incheckout_oldonly. 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
- Polymorphism is one call that does different work, chosen by the object that receives it.
PaymentMethod()fails with aTypeError, because an abstract class cannot be made into an object. - The first bill: 140 is at most 200, so it prints
Rs 140: paidand the balance is 200 - 140 = Rs 60. The second bill: 90 is more than 60, soCanteenCard.payraises,CardPaymentreturnsFalse, and it printsRs 90: refused. The balance stays Rs 60. - The same chain is in more than one place, and the teammate edited only one. Write
NetBankingPaymentas 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
Protocolare 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
isinstancechain is the same if-chain.
11Sources
- Abstract classes cannot be made until every abstract method is filled in: Python documentation,
abc, https://docs.python.org/3/library/abc.html (opens in a new tab) (checked 29 Oct 2026). - Protocol and static duck typing, checked by type checkers: Python documentation,
typing, https://docs.python.org/3/library/typing.html (opens in a new tab) (checked 29 Oct 2026). - Java interfaces and
implements: Oracle Java Tutorials, https://docs.oracle.com/javase/tutorial/java/IandI/createinterface.html (opens in a new tab) (checked 29 Oct 2026). - The canteen, the bill, the limits and the voucher are made up for teaching. The Python code was run with Python 3.11 and the Java box with Java 17.
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.
What to learn next
Continue learning with the lessons that build on this one.
- Command: undo and redo
- SOLID with small examples
- Clarify first: questions, assumptions and the rules list
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.