Skip to content
FollowtheDamnCode

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?

InterfaceAbstract class
HoldsMethod signatures only (Java 8+ allows defaults)Signatures plus shared code and state
A class can haveManyOne parent (in single-inheritance languages)
Use whenUnrelated classes share a capability (Comparable, Serializable)Related classes share code and a skeleton
Pythontyping.Protocol or an ABC with only abstract methodsabc.ABC with some concrete methods
Checkout depends on the role, never on a provider

In code

An abstract base that refuses half-built gateways
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: TypeError

Run 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

ProsCons
Swap implementations freely; trivial test doublesExtra types to read and navigate
Implementers get a checklistPremature interfaces add ceremony
Callers depend on stable abstractions (Dependency Inversion)Abstract classes still bind you to one parent

How it relates

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.