The Dependency Inversion Principle (DIP) has two parts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
The short version: depend on interfaces, not concrete classes. It's what lets you swap a database, mock a service in tests, or change an email provider without rewriting business logic.
The idea#
You don't hardwire your TV into the building's wiring. You plug it into a standard socket. Any device with the right plug works with any socket, and you can swap devices without rewiring the house. The socket is the abstraction.
Violation#
UserService builds its own MySQL connection, so it is welded to that database:
class MySQLDatabase: # low-level detail
def save_data(self, data: str) -> None:
print(f"Saving to MySQL: {data}")
class UserService: # high-level policy
def __init__(self):
# Hardwired: switching databases means editing this class
self._database = MySQLDatabase()
def save_user(self, user: str) -> None:
self._database.save_data(user)
UserService().save_user("ana")
Fix#
Introduce an abstraction that the service owns, and pass an implementation in:
from abc import ABC, abstractmethod
class Database(ABC): # the abstraction
@abstractmethod
def save_data(self, data: str) -> None: ...
class MySQLDatabase(Database):
def save_data(self, data: str) -> None:
print(f"Saving to MySQL: {data}")
class MongoDatabase(Database):
def save_data(self, data: str) -> None:
print(f"Saving to MongoDB: {data}")
class UserService:
def __init__(self, database: Database): # dependency is injected
self._database = database # depends only on the abstraction
def save_user(self, user: str) -> None:
self._database.save_data(user)
mysql_service = UserService(MySQLDatabase())
mongo_service = UserService(MongoDatabase()) # no change to UserService
mongo_service.save_user("ana")
Benefits:
- Swap implementations without touching high-level code.
- Test easily by injecting a mock or in-memory database.
- Code is more flexible and easier to maintain.
The "inversion" is that the dependency arrow now points from the concrete database up to the interface that the high-level module owns, instead of from the service down to the database.
DIP vs dependency injection#
They're related but not the same thing:
| Dependency Inversion (DIP) | Dependency Injection (DI) | |
|---|---|---|
| What it is | A principle: what to do | A technique: how to do it |
| Says | Depend on abstractions | Pass dependencies in from outside instead of creating them inside |
class Engine:
def start(self) -> str:
return "vroom"
class V8Engine(Engine):
pass
# Violates DIP: creates its own dependency
class HardwiredCar:
def __init__(self):
self._engine = V8Engine()
# Follows DIP via DI: depends on Engine, receives an implementation
class Car:
def __init__(self, engine: Engine):
self._engine = engine
Car(V8Engine())
DI is the main way you implement DIP.
Three kinds of injection#
A dependency can arrive through the constructor, a setter, or the method that needs it:
from typing import Protocol
class Database(Protocol):
def save(self, user: str) -> None: ...
# 1. Constructor injection (most common)
class ConstructorUserService:
def __init__(self, db: Database):
self._db = db
# 2. Setter injection
class SetterUserService:
def __init__(self):
self._db: Database | None = None
def set_database(self, db: Database) -> None:
self._db = db
def save_user(self, user: str) -> None:
self._db.save(user) # fails if set_database was never called
# 3. Method injection
class MethodUserService:
def save_user(self, user: str, db: Database) -> None:
db.save(user)
At the composition root (e.g. main()), you build the concrete objects and wire them together:
database = MySQLDatabase()
email = SmtpEmailSender()
service = UserService(database, email)
# In tests, inject fakes instead
test_service = UserService(InMemoryDatabase(), email)
Why constructor injection is usually best#
Constructor injection wins for three reasons:
- Dependencies are guaranteed. You can't create the object without them, so they're never
None. - The object is ready immediately. With setter injection, forgetting
set_database()leads to anAttributeError(or aNonedereference) later. - Stability. With no setter, the dependency can't be swapped out unexpectedly. Annotate it as
Finalif you want a type checker to enforce that.
service = SetterUserService() # setter-injection style
service.save_user("ana") # AttributeError: _db is None
| Injection | Use for |
|---|---|
| Constructor | Required dependencies; the default choice |
| Setter | Optional dependencies, or ones that legitimately change later |
| Method | Dependencies that vary per call |
All three in one class:
class EmailService:
def __init__(self, server: SmtpServer): # required: constructor
self._server = server
self._logger: Logger | None = None
def set_logger(self, logger: Logger) -> None: # optional: setter
self._logger = logger
# Varies per call: method injection
def send_email(self, email: Email, formatter: Formatter) -> None:
self._server.send(formatter.format(email))
A Car that can't work without an Engine should take it through the constructor.
All five principles together#
DIP usually ties the other four together. This small storage example uses every SOLID principle:
from abc import ABC, abstractmethod
# I: small, focused interfaces
class Readable(ABC):
@abstractmethod
def read(self) -> str: ...
class Writable(ABC):
@abstractmethod
def write(self, data: str) -> None: ...
# D: the abstraction high-level code depends on
class Storage(Readable, Writable):
pass
# O: new providers extend this; nothing existing changes
class CloudStorage(Storage):
def __init__(self, endpoint: str):
self._endpoint = endpoint
# S: each class handles one storage provider
# L: each is a drop-in Storage, honoring read/write fully
class S3Storage(CloudStorage):
def read(self) -> str:
return f"Reading from S3: {self._endpoint}"
def write(self, data: str) -> None:
print(f"Writing to S3: {data}")
class AzureBlobStorage(CloudStorage):
def read(self) -> str:
return f"Reading from Azure Blob: {self._endpoint}"
def write(self, data: str) -> None:
print(f"Writing to Azure Blob: {data}")
# D: high-level class receives its dependency via the constructor
class DataService:
def __init__(self, storage: Storage):
self._storage = storage
def process_data(self) -> None:
data = self._storage.read()
self._storage.write(f"Processed: {data}")
if __name__ == "__main__":
DataService(S3Storage("s3://bucket")).process_data()
# Same DataService code, different provider
DataService(AzureBlobStorage("azure://container")).process_data()
Which principle lets you swap S3 for Azure without touching DataService? Dependency Inversion: DataService depends on the Storage interface and gets a concrete instance injected.
Which principles reduce coupling?#
Of the five, only DIP targets coupling head-on:
| Principle | Effect on coupling |
|---|---|
| SRP | Indirect: separates concerns, but split classes can still be tightly bound |
| OCP | Some: introduces abstractions, but doesn't say how dependencies are wired |
| LSP | Minimal: makes inheritance safe to rely on |
| ISP | Partial: smaller interfaces mean clients depend on less |
| DIP | Direct: "don't create dependencies, receive them" |
Practice#
An EmailService creates a GmailSender inside itself. How do you refactor it? Introduce an EmailSender interface, have GmailSender implement it, and inject an EmailSender through EmailService's constructor.
Key takeaways#
- High-level code and low-level details should both depend on abstractions.
- DIP is the principle; dependency injection is the technique.
- A class that constructs its own collaborators is a DIP red flag.
- Prefer constructor injection for required dependencies, and don't expose setters for them.
- DIP is what makes unit testing with mocks straightforward.