Dependency Inversion Principle

RoadmapsPython

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

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

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

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.

Return to Python Roadmap