Abstraction and Interfaces
Depend on what something does, not how it does it. Interfaces name the behavior and hide the implementation.
Level: intermediate
What is it?
Abstraction separates what a component does from how it does it. An interface (or abstract class, protocol, or trait) declares a set of operations without implementing them. Callers depend on the interface; any class that fulfills it can be plugged in. The interface is the contract, and it is the part that must stay stable.
Why does it matter?
Real systems change: a database becomes a different database, a file becomes a network call. When callers depend on an interface, you swap the implementation without touching them, and you can substitute a fake in tests.
Mental model
A wall socket. Every appliance depends on the socket's shape, not on the power plant behind it. You can change the power plant, or plug in a generator, and nothing plugged in needs to know.
How it works
- An interface is a contract: the operations, their inputs, outputs, and rules — not the code.
- Program to an interface: parameters and fields should have the abstract type, the constructor picks the concrete one.
- Abstract classes can share code among subclasses; interfaces (and protocols) only define the contract and allow many implementations.
- Keep interfaces small and focused on what callers need. Wide interfaces leak implementation and are hard to satisfy.
- Abstractions have a cost: indirection. Introduce them where change or testing actually needs a seam, not everywhere.
Steps
- Name the behavior callers need
- Declare it as an interface
- Implement it in classes
- Inject the implementation
- Swap it without touching callers
Code example: Depend on the contract, plug in any implementation
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def save(self, key, data): ...
@abstractmethod
def load(self, key): ...
class MemoryStorage(Storage):
def __init__(self):
self._d = {}
def save(self, key, data):
self._d[key] = data
def load(self, key):
return self._d[key]
def backup(store: Storage, key, data): # knows only the interface
store.save(key, data)
store = MemoryStorage()
backup(store, "note", "hello")
print(store.load("note")) # helloReal-world usage
- Repository, service, and gateway boundaries around databases, queues, and APIs
- Plug-in architectures and drivers
- Test doubles: swapping a real network call for an in-memory fake
Common mistakes
- Creating an interface for every class, with exactly one implementation and no reason to change.
- Interfaces that mirror one implementation's quirks.
- Leaking implementation details, such as SQL rows, through the abstraction.
Interview questions
Abstract class versus interface?
An interface only declares behavior, and a class can implement many. An abstract class can also carry state and shared code but a class usually inherits from only one. Use an interface for a contract, an abstract class to share implementation among related classes.
What does 'program to an interface, not an implementation' mean?
Declare variables and parameters with the abstract type, so the code works with any implementation and the concrete class is chosen in one place.
What is a leaky abstraction?
One whose implementation details escape through the interface — error types, performance behavior, or data formats — so callers must know how it works underneath to use it correctly.