The Problem: Tight Coupling
When a class creates its own dependencies, it is tightly coupled to their concrete implementations. Changing one dependency means changing the class itself, and testing becomes painful because you can’t swap in a fake.The Solution: Inject from Outside
Move dependency creation to the caller. The class just declares what it needs in its constructor and uses whatever it receives.StudentService doesn’t care how the repository works — only that it has a get_all() method.
Why DI Improves Testability
Swapping the real repository for a fake one in tests becomes trivial:Injecting Interfaces Instead of Concrete Classes
For maximum flexibility, depend on an Abstract Base Class (an interface) rather than a concrete class. Any class that implements the ABC can be swapped in.Types of Dependency Injection
Constructor Injection (most common)
Constructor Injection (most common)
Dependencies are passed as constructor parameters. This is the most transparent form — you can see all dependencies at a glance.
Method (Parameter) Injection
Method (Parameter) Injection
The dependency is passed directly to the method that needs it. Useful when only one method requires the dependency, or when the dependency varies per call.
Setter Injection
Setter Injection
Dependencies are set through a method after construction. Less common in Python — prefer constructor injection for required dependencies.
Benefits of Dependency Injection
Testability
Inject mock or in-memory implementations during tests. No real databases, no network calls, no filesystem access — just fast, predictable unit tests.
Flexibility
Swap a SQLite database for PostgreSQL, a fake email service for SendGrid, or a local file store for S3 — without changing the classes that use them.
Separation of concerns
Each class has one job: using its dependencies. The responsibility for creating and wiring dependencies lives in one place (your application entry point or DI framework).
Reusability
A service that accepts an interface rather than a concrete class can be reused across projects, teams, and contexts with minimal changes.
How FastAPI Implements DI with Depends()
FastAPI has a built-in DI system. You define dependency functions and declare them on route parameters using Depends(). FastAPI resolves and injects them automatically on every request.
get_repository(), passes the result to get_service(), and passes that result to your endpoint — all before your handler function runs.
Yield dependencies for resource management
When a dependency needs setup and teardown (like a database session), useyield:
Quick Summary
1
Identify dependencies
Any object your class needs to do its work — database, external service, logger, configuration — is a dependency.
2
Accept them from outside
Declare dependencies as constructor parameters rather than creating them inside
__init__. Annotate with an abstract type where possible.3
Wire them at the entry point
In a plain Python app, wire dependencies in
main.py or an application factory. In FastAPI, use Depends() functions.4
Swap for tests
In your test file, construct classes with mock or in-memory implementations. Your business logic classes need zero changes.