Interfaces and Abstract Classes
Contracts without (or with only some) implementation — how you say what a role must do before anyone decides how.
Big idea
An interface is a promise: anything playing this role can do these operations. An abstract class is a promise plus some shared, half-built code. Both let you program against the role, not the concrete class — the move that makes swapping, testing, and extending cheap.
The problem it solves
A checkout service calls StripeGateway().charge(amount) directly. Now:
- Tests hit the real payment provider (or can't run offline).
- Adding a second provider means
if provider == "stripe" … else …inside checkout. - Nothing in the code says what a "gateway" must support; the next one may forget
refund().
Naming the role — PaymentGateway with charge and refund — gives checkout something stable to depend on and gives implementers a checklist.
Think of it like
A job description versus an employee. "Night-shift pharmacist: must dispense, must check interactions, must keep the register" is the interface — it doesn't name a person. Any qualified pharmacist can fill the shift. An abstract class is a job description that also comes with a training manual: some steps are already written down for you, and you fill in the rest.
Interface or abstract class?
| Interface | Abstract class | |
|---|---|---|
| Holds | Method signatures only (Java 8+ allows defaults) | Signatures plus shared code and state |
| A class can have | Many | One parent (in single-inheritance languages) |
| Use when | Unrelated classes share a capability (Comparable, Serializable) | Related classes share code and a skeleton |
| Python | typing.Protocol or an ABC with only abstract methods | abc.ABC with some concrete methods |
In code
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def charge(self, cents): ...
@abstractmethod
def refund(self, charge_id): ...
def charge_or_fail(self, cents): # shared code: an abstract class can carry it
result = self.charge(cents)
if result is None:
raise RuntimeError("charge declined")
return result
class FakeGateway(PaymentGateway): # a test double is just another implementer
def __init__(self):
self.log = []
def charge(self, cents):
self.log.append(f"charge {cents}")
return f"ch_{len(self.log)}"
def refund(self, charge_id):
self.log.append(f"refund {charge_id}")
class Checkout:
def __init__(self, gateway: PaymentGateway): # depends on the role
self.gateway = gateway
def pay(self, cart):
return self.gateway.charge_or_fail(sum(cart.values()))
fake = FakeGateway()
print(Checkout(fake).pay({"book": 1200, "pen": 150}))
print(fake.log)
class HalfDone(PaymentGateway):
def charge(self, cents):
return "x"
try:
HalfDone()
except TypeError as err:
print("refused:", type(err).__name__)output
ch_1
['charge 1350']
refused: TypeErrorRun and edit it in the app.
Forgetting refund() is caught the moment someone tries to create a HalfDone — not months later when a customer asks for their money back.
Printer, Scanner, and Fax are unrelated classes, but all can report a status. What fits best?
Watch it
Protocols: interfaces without inheritance
Python's typing.Protocol describes a role structurally: any class with the right methods matches, even one from a library you can't edit. A type checker enforces it; at runtime it's ordinary duck typing.
from typing import Protocol
class SupportsCharge(Protocol):
def charge(self, cents: int) -> str: ...
When to use them — and when not to
Introduce an interface at a seam: where you need a test double, where a second implementation is real or likely, or where a module boundary should hide details. Use an abstract class when implementers share real code and a fixed skeleton (Template Method).
Don't create IUserService for a class that will only ever have one implementation and is never mocked — that's ceremony (YAGNI). You can extract the interface the day the second implementation arrives.
Pros and cons
| Pros | Cons |
|---|---|
| Swap implementations freely; trivial test doubles | Extra types to read and navigate |
| Implementers get a checklist | Premature interfaces add ceremony |
| Callers depend on stable abstractions (Dependency Inversion) | Abstract classes still bind you to one parent |
How it relates
- Polymorphism is what happens when you call through one.
- Interface Segregation says keep them small; Dependency Inversion says high-level code should own them.
- Nearly every pattern diagram in this track has an
<<interface>>box at its centre.
A subclass of a Python ABC doesn't implement one of the @abstractmethod methods. When is that caught?
Key terms
- Interface — A named set of operations a class promises to provide, with no (or minimal) implementation.
- Abstract class — A class that can't be instantiated, mixing abstract methods with shared concrete code.
- Abstract method — A method declared without a body that every concrete subclass must implement.
- Protocol — A structural interface: any class with matching methods satisfies it, no inheritance needed.
- Seam — A place in the code where behaviour can be swapped without editing the caller.