python-dependency-injection

SKILL.md

Python Dependency Injection

Dependency Injection (DI) is a design pattern where a class receives its dependencies from an external source rather than constructing them internally. This decouples components, improves testability, and enables flexible configuration without modifying production code.

Core Concept: Inversion of Control

Inversion of Control (IoC) shifts responsibility for creating and managing dependencies from the dependent class to an external orchestrator (a container or the caller). The class declares what it needs; something else provides it.

Tight coupling (avoid):

class UserNotifier:
    def __init__(self):
        self.email = EmailService()  # hard-coded, untestable

    def notify(self, msg):
        self.email.send(msg)

Loose coupling via DI (prefer):

class UserNotifier:
    def __init__(self, email_service: EmailService):
        self.email_service = email_service  # injected, swappable

    def notify(self, msg):
        self.email_service.send(msg)

notifier = UserNotifier(EmailService())

Injection Styles

Style How When to use
Constructor Pass via __init__ Default; dependencies are required
Setter Assign via method after construction Optional dependencies
Method Pass directly to the method call One-off or per-call dependencies

Constructor injection is the preferred style. It makes all dependencies explicit and visible at object creation time.

The dependency-injector Library

For production applications, use the dependency-injector package (v4.x, BSD licensed, Python ≥ 3.8):

pip install dependency-injector

Container-Provider Architecture

The framework organizes everything around two primitives:

  • Container — the central registry; declares all dependencies and how they are built
  • Provider — defines how a single dependency instance is created
from dependency_injector import containers, providers

class Container(containers.DeclarativeContainer):
    config = providers.Configuration()

    api_client = providers.Singleton(
        ApiClient,
        api_key=config.api_key,
        timeout=config.timeout,
    )

    service = providers.Factory(
        Service,
        api_client=api_client,
    )

Provider Types

Provider Behavior Use case
Singleton Single shared instance for entire app lifetime DB connections, API clients, caches
Factory New instance on every call Request handlers, per-operation objects
Configuration Reads from env vars, YAML, JSON, ini App settings
Resource Managed lifecycle with setup/teardown DB sessions, file handles
Callable Wraps any callable Functions, class methods
Object Provides a fixed value Constants, pre-built objects
Selector Selects a provider based on config Environment-based switching

Configuration Provider

Load settings from multiple sources:

class Container(containers.DeclarativeContainer):
    config = providers.Configuration()

container = Container()
container.config.api_key.from_env("API_KEY", required=True)
container.config.timeout.from_env("TIMEOUT", as_=int, default=5)
# Also supports: .from_yaml(), .from_ini(), .from_dict(), .from_pydantic()

Wiring: Auto-inject into Functions

Wiring eliminates manual dependency passing in function calls. Decorate a function with @inject, mark parameters with Provide[...], then call container.wire():

from dependency_injector.wiring import inject, Provide

@inject
def main(service: Service = Provide[Container.service]) -> None:
    service.do_work()

if __name__ == "__main__":
    container = Container()
    container.config.from_env(...)
    container.wire(modules=[__name__])
    main()  # service is injected automatically

Wire entire packages at once: container.wire(packages=["myapp"]).

Overriding for Tests

Override any provider without modifying application code:

# In tests
with container.api_client.override(mock.Mock()):
    main()  # the mock is injected instead

Or using the dependency-injector testing helpers:

def test_service_behavior():
    container = Container()
    container.db.override(providers.Factory(FakeDB))

    service = container.service()
    assert service.get_data() == "expected"

FastAPI equivalent via dependency_overrides:

app.dependency_overrides[get_db_session] = lambda: FakeDBSession()

Resource Provider: Managed Lifecycle

Use Resource for dependencies that require explicit setup and teardown:

from dependency_injector import resources

class DatabaseResource(resources.Resource):
    def init(self) -> Database:
        db = Database(url=self.config.db_url())
        db.connect()
        return db

    def shutdown(self, db: Database) -> None:
        db.disconnect()

class Container(containers.DeclarativeContainer):
    config = providers.Configuration()
    db = providers.Resource(DatabaseResource, config=config)

# Usage
container = Container()
container.init_resources()
# ... use container.db() ...
container.shutdown_resources()

Async Injection

The framework supports async resources and FastAPI's native Depends():

# dependency-injector async resource
class AsyncDBResource(resources.AsyncResource):
    async def init(self) -> AsyncDB:
        db = AsyncDB()
        await db.connect()
        return db

    async def shutdown(self, db: AsyncDB) -> None:
        await db.disconnect()

# FastAPI native async dependency
async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with async_session() as session:
        yield session

@app.get("/items")
async def read_items(db: AsyncSession = Depends(get_db)):
    ...

Best Practices

Program to abstractions:

from abc import ABC, abstractmethod

class MessageSender(ABC):
    @abstractmethod
    def send(self, message: str) -> None: ...

class EmailSender(MessageSender):
    def send(self, message: str) -> None:
        print(f"Email: {message}")

class SMSSender(MessageSender):
    def send(self, message: str) -> None:
        print(f"SMS: {message}")

# Container switches implementation without changing consumer
class Container(containers.DeclarativeContainer):
    sender = providers.Factory(EmailSender)  # swap to SMSSender anytime

Centralize dependency configuration — define all providers in one container module, not scattered across the codebase.

Avoid circular dependencies — if A depends on B and B depends on A, restructure or use a factory provider to delay instantiation.

Use the right scopeSingleton for stateless shared resources; Factory for stateful per-request objects. Mismatched scopes cause subtle state leakage bugs.

Lock dependency versions — pin exact versions in a lock file (poetry.lock, pip-compile output) to avoid dependency confusion attacks.

Anti-Patterns to Avoid

Anti-pattern Problem Fix
Service Locator container.get(Service) inside business logic hides dependencies Inject explicitly via constructor or @inject
Over-injection 10+ constructor params Split into smaller, focused classes
Tight coupling self.dep = ConcreteClass() inside __init__ Accept dependency as parameter
Scope mismanagement Singleton wrapping a stateful request-scoped object Use Factory or Resource with correct scope
Monkey-patching in tests module.SomeClass = MockClass Use container.override() or dependency_overrides

Quick Reference

pip install dependency-injector          # base
pip install "dependency-injector[yaml]"  # + YAML config support
pip install "dependency-injector[pydantic2]"  # + Pydantic v2 settings
# Minimal working container
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide

class Container(containers.DeclarativeContainer):
    config  = providers.Configuration()
    service = providers.Factory(MyService, setting=config.setting)

@inject
def handler(svc: MyService = Provide[Container.service]):
    svc.run()

container = Container()
container.config.from_dict({"setting": "value"})
container.wire(modules=[__name__])
handler()

Additional Resources

For detailed coverage of advanced topics, consult:

  • references/patterns.md — Scoped dependencies, multiple containers, selector providers, and testing patterns
  • references/framework-integration.md — Step-by-step integration with Flask, Django, and FastAPI including wiring setup
Weekly Installs
2
GitHub Stars
1
First Seen
1 day ago
Installed on
opencode2
amp1
cline1
cursor1
kimi-cli1
codex1