Dependency Injection
What is dependency injection (DI)?
Dependency injection is a design pattern where a component receives its dependencies from the outside instead of creating or looking them up itself. The caller (or a container/framework) provides the concrete implementations—e.g. a database connection, a logger, or an API client—so the component stays decoupled from those details and is easier to test and change.
What are the benefits of dependency injection?
- Loose coupling: Components depend on abstractions or contracts, not on concrete classes.
- Testability: In tests you inject mocks or stubs (e.g. fake DB, in-memory repo) instead of real implementations.
- Flexibility: You can swap implementations (e.g. different DB, different LLM provider) without changing the component.
- Single responsibility: The component focuses on its logic; something else is responsible for wiring dependencies.
- Explicit dependencies: What a component needs is visible in its constructor or parameters.
How do you implement dependency injection in Python?
- Constructor injection: Pass dependencies in
__init__; store them as attributes. The caller (or a factory) creates the dependencies and the component.
class UserService:
def __init__(self, repo: UserRepository, logger: Logger):
self.repo = repo
self.logger = logger
def get_user(self, id: str):
return self.repo.find_by_id(id)
- Function/setter injection: Pass dependencies as arguments to a function or set them via setters. Common in frameworks (e.g. FastAPI
Depends()injects into route handlers). - Container / framework: Use a DI container (e.g.
dependency-injector, or FastAPI’sDepends) to register and resolve dependencies; the framework injects them when creating the component.
How does FastAPI use dependency injection?
FastAPI’s Depends() is a form of DI: you declare dependencies (functions or callables) and FastAPI calls them and injects the return value into the route (or into other dependencies). Used for: DB sessions, auth, config, shared services. Dependencies can be nested (a dependency can depend on another). The framework creates the dependency graph and injects at request time.
What is the difference between DI and a service locator?
- Dependency injection: The component receives its dependencies (passed in or injected by a framework). It doesn’t know how they were created.
- Service locator: The component asks a central registry (“give me the user repo”) when it needs something. The component still depends on the locator and on knowing what to ask for.
DI is generally preferred: dependencies are explicit and easier to test; you don’t hide them behind a global lookup.
Interview angle
- “What is dependency injection, in one sentence?” - a component receives its collaborators from outside rather than constructing them, so the caller decides which implementation it gets.
- “Why does it matter beyond testing?” - it makes the dependency graph explicit and swappable. Testing is the most visible benefit, but the structural one is that a component’s constructor documents everything it needs, with no hidden globals.
- “DI versus the dependency inversion principle?” - DI is the mechanism (pass it in); DIP is the design rule (depend on abstractions, and let the high-level module own the abstraction). You can do DI without DIP by injecting concrete classes.
- “Do you need a container?” - not for a flat graph; constructor arguments wired in a composition root are enough. A container earns its place with deep graphs, differing lifetimes, or when the same graph is needed outside HTTP - Celery tasks and consumers. See 07_di_pattern_library.md.