Concept #165Mediumpython-for-gen-aiimportant

How to use Dependency Injection in Python?

#python#dependency-injection#di#fastapi#solid#testing#design-patterns#inversion-of-control

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

TypeHowWhen to Use
Constructor InjectionPass via
text
__init__
Most common — required dependencies
Method InjectionPass via method argumentOptional, per-call dependencies
Property InjectionSet as attribute after initRare — optional post-construction config
Framework InjectionFramework calls your function with depsFastAPI
text
Depends
,
text
dependency-injector

1. Constructor Injection (Manual DI)

The simplest and most Pythonic form of DI.

python
from 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.

python
class 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 (
text
Depends
)

FastAPI has a built-in DI system using

text
Depends
— the most common DI pattern in Python web/API Gen AI apps.

python
from 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 —

text
get_pipeline
depends on
text
get_llm_client
which depends on
text
get_settings
. You only call
text
Depends(get_pipeline)
.


4.
text
dependency-injector
Library

A dedicated DI framework for larger Python applications — supports containers, providers, and wiring.

python
from 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:

text
pip install dependency-injector


5. Testing with DI — Swapping Dependencies

DI makes unit testing trivial — inject mocks instead of real implementations.

python
import 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
text
dataclasses
and
text
__post_init__

A lightweight pattern using

text
@dataclass
for DI without extra libraries.

python
from 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

PatternLibraryBest For
Constructor injectionNoneAll projects — foundational
Method injectionNonePer-call optional deps
text
Depends
FastAPIAPI routes, async apps
text
DeclarativeContainer
text
dependency-injector
Large apps, microservices
text
@dataclass
+
text
__post_init__
NoneLightweight with sensible defaults
pytest fixturespytestUnit/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

text
gpt-4o
for
text
claude-3-5-sonnet
or
text
ChromaDB
for
text
Pinecone
with a single line change at the composition root.

Learn more at FastAPI Dependencies, dependency-injector docs, and Python DIP Guide.