Abstraction and Interfaces¶
Expose what a component can do, and hide the messy details of how it does it.
After classes, encapsulation, and inheritance, abstraction answers the next design question: what clean contract should the rest of the system depend on?
Why This Matters¶
A checkout page should be able to say process payment without caring whether the implementation uses card, wallet, or bank transfer. A reporting screen should be able to say export report without depending on CSV-specific details.
Abstraction helps teams:
design systems around capabilities rather than concrete implementations
swap implementations with minimal changes
test logic with lightweight fakes or mocks
reduce coupling across growing applications
Learning Goals¶
distinguish abstraction from encapsulation
use abstract base classes to define required behavior
explain what an interface-like contract means in Python
prepare for composition, where abstractions become reusable building blocks
Abstraction vs Encapsulation¶
These ideas work together, but they are not the same:
| Idea | Main question | Example |
|---|---|---|
| Encapsulation | How do we protect valid state? | BankAccount.withdraw() checks balance rules |
| Abstraction | What capability should callers depend on? | PaymentProcessor.process(amount) hides gateway details |
Encapsulation protects the inside. Abstraction simplifies the outside.
Visual Intuition¶
Caption: The checkout workflow depends on a contract, not on one specific gateway implementation.*
Worked Example: Payment Processor Contract¶
The function complete_checkout() only needs a processor with the required behavior. That is the value of abstraction: fewer assumptions, more flexibility.
from abc import ABC, abstractmethod
class ReportExporter(ABC):
@abstractmethod
def export(self, records):
pass
class CSVExporter(ReportExporter):
def export(self, records):
header = 'name,revenue'
rows = [f'{name},{revenue}' for name, revenue in records]
return '\n'.join([header] + rows)
class TextExporter(ReportExporter):
def export(self, records):
return '\n'.join(f'{name}: ${revenue:,.0f}' for name, revenue in records)
records = [('Retail', 42000), ('Subscriptions', 18500)]
for exporter in (CSVExporter(), TextExporter()):
print(type(exporter).__name__)
print(exporter.export(records))
print('-' * 20)Guided Practice¶
Quick MCQ¶
Which design choice best demonstrates abstraction?
A) A checkout function depends on
PaymentProcessor.process()instead of one specific gateway classB) A class exposes ten public variables and no methods
C) A programmer copies the same payment code into five files
Correct answer: A
Reflection Questions¶
Why is it useful that
complete_checkout()does not know which processor it receives?Which part of the system should change when a new payment gateway is added?
How does abstraction reduce coupling between teams?
Answer Check
It stays reusable and easier to test.
Usually a new implementation class, not the checkout workflow itself.
Teams can work against agreed contracts without depending on each other’s internal details.
Exercises¶
Exercise 1¶
Create a NotificationChannel abstraction with two implementations: EmailChannel and SMSChannel.
Exercise 2¶
Write a send_campaign(channel, message) function that works with either implementation without knowing channel details.
Exercise 3¶
Review one earlier class and separate its public contract from its internal helper methods. Which behaviors should callers know about, and which should remain internal?
Starter Hint
Begin by writing one abstract method such as send() or export(). Then implement two concrete subclasses and pass them into the same function.
Key Takeaway¶
Abstraction defines the clean surface area of a system. Callers depend on what an object can do, not on how that behavior is implemented.
This leads naturally to composition versus inheritance, where those abstractions become the seams that let us assemble flexible systems from smaller parts.