How to use Dependency Injection in Python?
Answer
Dependency Injection in Python
Dependency Injection (DI) is a design pattern where an object receives its dependencies from the outside rather than creating them itself. It is the practical implementation of the Dependency Inversion Principle (DIP) from SOLID.
Why Dependency Injection?
python# Without DI — tightly coupled, hard to test class RAGPipeline: def __init__(self): self.llm = OpenAIClient(api_key="sk-abc") # hardcoded self.db = ChromaDB(path="./chroma") # hardcoded # With DI — loosely coupled, easy to swap/test class RAGPipeline: def __init__(self, llm: BaseLLM, db: VectorStore): self.llm = llm # injected self.db = db # injected
Types of Dependency Injection
| Type | How | When to Use |
|---|---|---|
| Constructor Injection | Pass via text | Most common — required dependencies |
| Method Injection | Pass via method argument | Optional, per-call dependencies |
| Property Injection | Set as attribute after init | Rare — optional post-construction config |
| Framework Injection | Framework calls your function with deps | FastAPI text text |
1. Constructor Injection (Manual DI)
The simplest and most Pythonic form of DI.
pythonfrom abc import ABC, abstractmethod # Abstractions class BaseLLM(ABC): @abstractmethod def generate(self, prompt: str) -> str: ... class BaseEmbedder(ABC): @abstractmethod def embed(self, text: str) -> list[float]: ... class BaseVectorStore(ABC): @abstractmethod def search(self, vector: list[float], k: int) -> list[str]: ... # Concrete implementations class OpenAILLM(BaseLLM): def __init__(self, model: str = "gpt-4o"): self.model = model def generate(self, prompt: str) -> str: return f"[{self.model}] {prompt[:40]}..." class OpenAIEmbedder(BaseEmbedder): def embed(self, text: str) -> list[float]: return [0.1, 0.2, 0.3] # mock class ChromaVectorStore(BaseVectorStore): def search(self, vector: list[float], k: int) -> list[str]: return [f"doc_{i}" for i in range(k)] # High-level class — depends on abstractions, not concretions class RAGPipeline: def __init__(self, llm: BaseLLM, embedder: BaseEmbedder, store: BaseVectorStore): self._llm = llm self._embedder = embedder self._store = store def run(self, query: str) -> str: vec = self._embedder.embed(query) docs = self._store.search(vec, k=5) prompt = f"Context:\n{chr(10).join(docs)}\n\nQuestion: {query}" return self._llm.generate(prompt) # Composition root — wire dependencies here only pipeline = RAGPipeline( llm=OpenAILLM(model="gpt-4o"), embedder=OpenAIEmbedder(), store=ChromaVectorStore(), ) print(pipeline.run("What is RAG?"))
2. Method Injection
Pass dependencies as function arguments — useful for optional or per-call deps.
pythonclass PromptBuilder: def build(self, query: str, retriever: BaseVectorStore, top_k: int = 5) -> str: docs = retriever.search([0.1, 0.2], k=top_k) return f"Context: {docs}\nQuestion: {query}" # Inject different retrievers per call builder = PromptBuilder() prompt_a = builder.build("What is LoRA?", retriever=ChromaVectorStore()) prompt_b = builder.build("Explain RLHF", retriever=PineconeStore())
3. FastAPI Dependency Injection (textDepends
)
DependsFastAPI has a built-in DI system using
Dependspythonfrom fastapi import FastAPI, Depends from pydantic_settings import BaseSettings from functools import lru_cache app = FastAPI() # --- Settings --- class Settings(BaseSettings): openai_api_key: str = "sk-test" openai_model: str = "gpt-4o" chroma_path: str = "./chroma_db" class Config: env_file = ".env" @lru_cache def get_settings() -> Settings: return Settings() # --- LLM Client --- def get_llm_client(settings: Settings = Depends(get_settings)) -> BaseLLM: return OpenAILLM(model=settings.openai_model) # --- Vector Store --- def get_vector_store(settings: Settings = Depends(get_settings)) -> BaseVectorStore: return ChromaVectorStore() # --- Pipeline --- def get_pipeline( llm: BaseLLM = Depends(get_llm_client), store: BaseVectorStore = Depends(get_vector_store), embedder: BaseEmbedder = Depends(OpenAIEmbedder), ) -> RAGPipeline: return RAGPipeline(llm=llm, embedder=embedder, store=store) # --- Route --- from pydantic import BaseModel class QueryRequest(BaseModel): question: str class QueryResponse(BaseModel): answer: str @app.post("/query", response_model=QueryResponse) async def query( request: QueryRequest, pipeline: RAGPipeline = Depends(get_pipeline), ): answer = pipeline.run(request.question) return QueryResponse(answer=answer)
Key benefit: FastAPI automatically resolves the full dependency graph —
depends ontextget_pipelinewhich depends ontextget_llm_client. You only calltextget_settings.textDepends(get_pipeline)
4. textdependency-injector
Library
dependency-injectorA dedicated DI framework for larger Python applications — supports containers, providers, and wiring.
pythonfrom dependency_injector import containers, providers from dependency_injector.wiring import Provide, inject # --- Container --- class AppContainer(containers.DeclarativeContainer): config = providers.Configuration() llm = providers.Singleton( OpenAILLM, model=config.openai.model, ) embedder = providers.Singleton(OpenAIEmbedder) vector_store = providers.Singleton(ChromaVectorStore) pipeline = providers.Factory( RAGPipeline, llm=llm, embedder=embedder, store=vector_store, ) # --- Wire into functions --- @inject def handle_query( query: str, pipeline: RAGPipeline = Provide[AppContainer.pipeline], ) -> str: return pipeline.run(query) # --- Bootstrap --- container = AppContainer() container.config.from_dict({ "openai": {"model": "gpt-4o"} }) container.wire(modules=[__name__]) result = handle_query("What is RAG?") print(result)
Install:
pip install dependency-injector5. Testing with DI — Swapping Dependencies
DI makes unit testing trivial — inject mocks instead of real implementations.
pythonimport pytest class MockLLM(BaseLLM): def generate(self, prompt: str) -> str: return "mocked response" class MockVectorStore(BaseVectorStore): def search(self, vector: list[float], k: int) -> list[str]: return ["mock_doc_1", "mock_doc_2"] class MockEmbedder(BaseEmbedder): def embed(self, text: str) -> list[float]: return [0.0, 0.0, 0.0] @pytest.fixture def pipeline(): return RAGPipeline( llm=MockLLM(), embedder=MockEmbedder(), store=MockVectorStore(), ) def test_pipeline_returns_response(pipeline): result = pipeline.run("What is RAG?") assert result == "mocked response" def test_pipeline_uses_retrieved_docs(pipeline): result = pipeline.run("Explain LoRA") assert isinstance(result, str) assert len(result) > 0
6. DI with textdataclasses
and text__post_init__
dataclasses__post_init__A lightweight pattern using
@dataclasspythonfrom dataclasses import dataclass, field @dataclass class RAGConfig: model: str = "gpt-4o" top_k: int = 5 temperature: float = 0.7 @dataclass class RAGPipelineV2: config: RAGConfig = field(default_factory=RAGConfig) llm: BaseLLM | None = None embedder: BaseEmbedder | None = None store: BaseVectorStore | None = None def __post_init__(self): # Default to real implementations if not injected if self.llm is None: self.llm = OpenAILLM(model=self.config.model) if self.embedder is None: self.embedder = OpenAIEmbedder() if self.store is None: self.store = ChromaVectorStore() def run(self, query: str) -> str: vec = self.embedder.embed(query) docs = self.store.search(vec, k=self.config.top_k) prompt = f"Docs: {docs}\nQ: {query}" return self.llm.generate(prompt) # Production — uses defaults prod_pipeline = RAGPipelineV2(config=RAGConfig(model="gpt-4o", top_k=10)) # Test — inject mocks test_pipeline = RAGPipelineV2( llm=MockLLM(), embedder=MockEmbedder(), store=MockVectorStore(), )
Choosing the Right DI Approach
Summary
| Pattern | Library | Best For |
|---|---|---|
| Constructor injection | None | All projects — foundational |
| Method injection | None | Per-call optional deps |
text | FastAPI | API routes, async apps |
text | text | Large apps, microservices |
text text | None | Lightweight with sensible defaults |
| pytest fixtures | pytest | Unit/integration testing |
Pro tip: In Gen AI engineering, always inject your LLM client and vector store — never instantiate them inside business logic. This lets you swap
fortextgpt-4oortextclaude-3-5-sonnetfortextChromaDBwith a single line change at the composition root.textPinecone
Learn more at FastAPI Dependencies, dependency-injector docs, and Python DIP Guide.