Dependency Inversion Principle
Overview
The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules; both should depend on abstractions. Furthermore, abstractions should not depend on details; details should depend on abstractions. In Python, this means your core business logic (high-level) shouldn't import concrete database or API classes (low-level). Instead, both should depend on an interface (like an abstract base class or `Protocol`, which defines expected methods). For example, instead of a `PaymentProcessor` instantiating a `StripeAPI` directly, it accepts any object that satisfies a `PaymentGateway` abstraction. Analogy: When you plug a lamp into a wall, the lamp (high-level) depends on a standard outlet interface (abstraction), not the specific wiring in the wall (low-level detail). class PaymentProcessor: def __init__(self, gateway: PaymentGateway = StripeAPI()): self.gateway = gateway A common sharp edge is binding a concrete class as a default argument, which tightly couples the modules and instantiates the dependency at module load time. Instead, default to `None` or use a dependency injection framework (covered fully in `dependency-injection`).
It decouples your system's core logic from volatile implementation details. This provides several major benefits: - Swap out components without rewriting business logic - Mock dependencies effortlessly during unit testing - Maintain code without breaking existing functionality
Where used: FastAPI dependency injection, Unit testing with mocks, Hexagonal/Clean Architecture
Why learn this
- Write highly testable code by easily swapping real dependencies with mocks
- Build modular applications where changing a database or API provider doesn't rewrite business logic
- Understand the foundation of modern frameworks like FastAPI and Django
Code walkthrough
from typing import Protocol
class Logger(Protocol):
def log(self, text: str) -> None:
...
class ConsoleLogger:
def log(self, text: str) -> None:
print(f'LOG: {text}')
class App:
def __init__(self, logger: Logger):
self.logger = logger
def run(self):
self.logger.log('Starting')
app = App(ConsoleLogger())
app.run()
Focus: app = App(ConsoleLogger())
Aha moment
class FakeDB:
def save(self, data):
print('Fake saved:', data)
class RealDB:
def save(self, data):
print('Real saved to disk:', data)
class UserCreator:
def __init__(self, db):
self.db = db
def create(self):
self.db.save('user1')
test_creator = UserCreator(FakeDB())
test_creator.create()
Prediction: What happens when `test_creator.create()` is called? Which DB is used?
Common guess: It saves to the real DB because `UserCreator` needs a database.
Because `UserCreator` depends on the abstraction of a database (anything with a `save()` method) rather than instantiating `RealDB` itself, we can inject `FakeDB` during tests, completely bypassing the disk.
Common mistakes
- Depending on concrete classes: Instead of instantiating a specific detail like `requests.Session()` internally, pass it into the constructor. This is known as Dependency Injection.
- Leaking implementation details into abstractions: Avoid naming methods after specific technologies (like `save_to_sql()`) in your interfaces. Keep method names generic (like `save()`) so they apply to any implementation.
- Using concrete classes as default arguments: Assigning `gateway: PaymentGateway = StripeAPI()` binds the concrete dependency at module load time. This completely defeats the decoupling DIP provides. Instead, default to `None` and instantiate if missing, or use a dependency injection container.
Glossary
- abstractions
- Simplified representations of complex systems, hiding the complicated details so you can focus on what the system does rather than how it works. Example: `class Logger:`
- decouples
- Separating different parts of a program so that a change in one part does not force you to change the other parts. Example: `def __init__(self, db):`
Recall questions
- What does the Dependency Inversion Principle state regarding high-level and low-level modules?
- Why does `def __init__(self, gateway: PaymentGateway = StripeAPI()):` violate DIP even though the parameter is typed as the abstraction?
Understanding checks
If high-level modules should not depend on low-level modules, how do they get access to the actual functionality they need?
High-level modules depend on abstractions (like an interface or `Protocol`), and the low-level modules implement those abstractions. The specific implementation is then passed to the high-level module, usually via dependency injection.
This is the essence of Dependency Inversion. The abstraction acts as a contract between the core logic and the details, without tightly coupling them.
What violates the Dependency Inversion Principle in the following code?
The `UserManager` directly instantiates the low-level `MySQLDatabase` concrete class, tightly coupling the core logic to the database implementation.
By directly calling `MySQLDatabase()`, the high-level module (`UserManager`) depends on the low-level module, rather than an abstraction. To fix this, it should accept the database instance as an argument in `__init__`.
Why does this `__init__` method violate the Dependency Inversion Principle despite using a type hint?
It provides a concrete class (`OpenWeatherMapAPI`) as the default argument. This creates a hard dependency on the low-level module.
Even though the parameter is typed as the `WeatherAPI` abstraction, the default value instantiates a specific detail. This forces the module to import and depend on `OpenWeatherMapAPI`, defeating the decoupling.
Practice tasks
Invert the Dependency
The `OrderProcessor` class below directly instantiates a `SMSSender`. Modify it to depend on the `MessageSender` abstraction and pass the concrete sender via the constructor.
Challenge
Implement an Abstract Data Store
Create a `UserRepository` protocol with a `get_user(user_id: int) -> str` method. Then, create a concrete `PostgresUserRepository` that returns `'User from DB'`. Finally, create a `UserService` that accepts a `UserRepository` and has a `get_user_info` method that calls `get_user`. Instantiate the service with the Postgres repository and call it.