Skip to main content
Dependency injection (DI) is a technique where an object receives the other objects it needs — its dependencies — from the outside rather than creating them internally. It sounds like a small shift, but it fundamentally changes how testable, maintainable, and flexible your code is. It’s also the core of how FastAPI handles database connections, authentication, and shared services.

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.
Now 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

Dependencies are passed as constructor parameters. This is the most transparent form — you can see all dependencies at a glance.
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.
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.
FastAPI builds the full dependency graph automatically: it calls 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), use yield:
In production FastAPI applications, your dependency chain typically looks like: get_db (database session) → get_repository(db)get_service(repository) → endpoint. Each layer is independently testable by injecting a mock at the appropriate level.

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.