Depend on Abstractions
The Dependency Inversion Principle says: high-level code — your business logic — should not depend on low-level details like a specific database or file format. Both should depend on an abstraction: in Python, a protocol.
The "inversion" is the direction of the arrow. Instead of the service pointing at one concrete storage, the service points at a protocol — and every storage points at that same protocol from the other side.
Hard-Wired to the Details
This user service creates and uses a concrete DiskStore directly. It works — but the service is welded to one storage. A different store, a faster store, a fake store for tests: every one of them means editing the service itself.
The service welded to its storage
UserService builds its own DiskStore inside. High-level registration logic now depends on a low-level detail — the file on disk.
Output: complete Python program
class DiskStore:
def save(self, user):
print("writing", user, "to file users.txt on disk")
# UserService builds its own storage — welded to the detail.
class UserService:
def __init__(self):
self.store = DiskStore()
def register(self, user):
print("registering user:", user)
self.store.save(user)
service = UserService()
service.register("ana")
Invert the Dependency
The fix: the service asks for a Storage protocol in its constructor and never learns which one it got. The disk store and the memory store both implement the protocol.
Now the arrow points from both sides to the abstraction — and swapping the storage is a one-line change at the place where the service is created, not inside it.
The service accepts a protocol
Same registration logic, two different storages. UserService(DiskStore()) or UserService(MemoryStore()) — the service code itself never changes.
Output: complete Python program
from typing import Protocol
# The abstraction both sides depend on.
class Storage(Protocol):
def save(self, user: str) -> None: ...
class DiskStore:
def save(self, user: str) -> None:
print("writing", user, "to file users.txt on disk")
class MemoryStore:
def save(self, user: str) -> None:
print("keeping", user, "in memory")
class UserService:
def __init__(self, store: Storage):
self.store = store
def register(self, user: str) -> None:
print("registering user:", user)
self.store.save(user)
on_disk = UserService(DiskStore())
on_disk.register("ana")
in_memory = UserService(MemoryStore())
in_memory.register("ben")
The payoff: testing without a disk
Because the service depends on the abstraction, a test can hand it a memory store and check the result instantly — no files, no cleanup. Dependency inversion is what makes code easy to unit-test.
Output: complete Python program
from typing import Protocol
class Storage(Protocol):
def save(self, user: str) -> None: ...
# MemoryStore records what was saved — perfect for checks.
class MemoryStore:
def __init__(self):
self.users: list[str] = []
def save(self, user: str) -> None:
self.users.append(user)
class UserService:
def __init__(self, store: Storage):
self.store = store
def register(self, user: str) -> None:
self.store.save(user)
store = MemoryStore()
service = UserService(store)
service.register("ana")
service.register("ben")
if len(store.users) == 2:
print("check passed: 2 users saved")
print("saved users:", store.users)