Abstract Classes and Interfaces

Topics Covered

Abstract Classes and Contracts

Why Abstract Methods Matter

Concrete Methods as Shared Behavior

When a Class Should Be Abstract

Interfaces and Multiple Contracts

What Is an Interface?

Interfaces Define Capabilities, Not Ancestry

Type Safety Through Interfaces

Abstract vs Interface, When to Use Each

Use an Abstract Class When...

Use an Interface When...

The Decision Framework

Designing with Interfaces

What "Program to Interfaces" Means

Designing Interfaces from the Client's Perspective

Interface as a Firewall

Dependency on Abstractions

The Dependency Problem

The Solution: Depend on Abstractions

Why This Matters at Scale

Practice Problems

Why can you not just use regular classes for everything? Because regular classes make promises they should not. When you create a class called Shape with an area() method that returns 0, you are lying to every developer who reads your code. A shape with zero area is not a shape: it is a placeholder pretending to be one. Abstract classes exist to solve this exact problem: they let you define a contract that says "every shape must compute its own area" without providing a fake default.

An abstract class is a class that cannot be instantiated directly. It exists solely as a blueprint for subclasses. It can have two kinds of methods: concrete methods (fully implemented, shared by all subclasses) and abstract methods (declared but not implemented: every subclass must provide its own version).

Abstract class with concrete and abstract methods

Why Abstract Methods Matter

Consider a payment system with credit cards, PayPal, and cryptocurrency. All payment methods share common behavior: they all log transactions, they all validate amounts, they all return a receipt. But the actual processing logic is completely different for each one. A credit card charges a card network. PayPal hits an API. Crypto submits a blockchain transaction.

python
1from abc import ABC, abstractmethod
2
3class PaymentMethod(ABC):
4    def process_payment(self, amount):
5        """Template: validate, process, log."""
6        self.validate(amount)
7        result = self.execute(amount)  # subclass provides this
8        self.log(amount, result)
9        return result
10
11    def validate(self, amount):
12        if amount <= 0:
13            raise ValueError("Amount must be positive")
14
15    def log(self, amount, result):
16        print(f"Processed ${amount}: {result}")
17
18    @abstractmethod
19    def execute(self, amount):
20        """Each payment type implements its own processing."""
21        pass

The PaymentMethod class provides validate() and log() as concrete methods: every payment type validates and logs the same way. But execute() is abstract. If you try to instantiate PaymentMethod directly, Python raises TypeError. You must create a subclass that provides execute().

python
1class CreditCardPayment(PaymentMethod):
2    def __init__(self, card_number):
3        self.card_number = card_number
4
5    def execute(self, amount):
6        return f"Charged ${amount} to card {self.card_number[-4:]}"
7
8class PayPalPayment(PaymentMethod):
9    def __init__(self, email):
10        self.email = email
11
12    def execute(self, amount):
13        return f"Charged ${amount} to PayPal {self.email}"

This is the Template Method pattern in action: the abstract class defines the algorithm skeleton (validate then execute then log), and subclasses fill in the variable steps. The base class controls the flow; the subclasses control the specifics.

Key Insight

Abstract classes are not about preventing instantiation: they are about forcing subclasses to make decisions. Every abstract method is a question the base class asks its children: how do you handle this specific responsibility? The compiler enforces that every child answers.

Concrete Methods as Shared Behavior

The real power of abstract classes is combining abstract and concrete methods. The concrete methods provide code reuse: every subclass gets validate() and log() for free. The abstract methods provide customization points: each subclass defines its own execute(). Without abstract classes, you would either duplicate the shared code in every subclass (violating DRY) or use a regular class with a fake default implementation (hiding bugs).

When a Class Should Be Abstract

Make a class abstract when:

  • Direct instantiation makes no sense. A generic "Animal" has no meaningful speak(). Only a Dog or Cat can speak.
  • Subclasses share behavior but differ in specifics. All database connectors validate queries and pool connections, but each executes queries differently.
  • You want the compiler to enforce completeness. If a developer adds a new payment type but forgets to implement execute(), the code will not compile.

The key insight is that abstract classes capture the parts of a design that are stable (the algorithm skeleton, the shared validations) while leaving the parts that vary (specific processing logic) to subclasses.