Dependency Injection
Dependency injection patterns for loosely coupled, testable Python applications
You are an expert in dependency injection patterns for writing loosely coupled, testable Python code. ## Key Points - **Constructor injection** passes dependencies via `__init__` parameters. - **Function/method injection** passes dependencies as function arguments. - **Protocol-based interfaces** define contracts without inheritance, enabling duck-typed DI. - **DI containers** (e.g., `dependency-injector`, `inject`, `punq`) automate wiring and lifecycle management. - **Factory functions** and **callables** act as lightweight providers. - **Scopes** control dependency lifecycle: singleton, per-request, transient. - Start with constructor injection — it is explicit, requires no framework, and works with any test runner. - Define dependencies as `Protocol` types rather than concrete classes to keep coupling loose. - Use factory functions for complex object graphs; reach for a DI container only when manual wiring becomes unwieldy. - Keep the composition root (where everything is wired together) in one place, typically near the application entry point. - Prefer `yield`-based dependencies (as in FastAPI) for resources that need cleanup. - Make dependencies explicit in function signatures rather than importing globals — this makes testing trivial.
skilldb get python-patterns-skills/Dependency InjectionFull skill: 262 linesDependency Injection — Python Patterns
You are an expert in dependency injection patterns for writing loosely coupled, testable Python code.
Overview
Dependency injection (DI) inverts control by passing dependencies into a component rather than having the component create them. In Python, DI is often achieved through constructor arguments, function parameters, or lightweight DI containers — without the heavy frameworks common in Java. The result is code that is easier to test, reconfigure, and extend.
Core Philosophy
Dependency injection is about making your code honest about what it needs. When a function or class declares its dependencies as explicit parameters rather than importing and constructing them internally, it tells you exactly what it requires to do its job. This honesty is the foundation of testability — you can swap any dependency with a fake, a mock, or an alternative implementation simply by passing a different argument.
In Python, DI should be lightweight and idiomatic. You do not need a Java-style framework with XML configuration files and annotation scanning. Constructor parameters, function arguments, and Python's Protocol-based structural typing give you everything you need. A DI container is useful when your object graph grows complex enough that manual wiring becomes tedious, but you should be able to understand and debug your wiring without the container.
The composition root pattern is the architectural discipline that holds DI together. All your wiring — which concrete classes fulfill which interfaces — should happen in one place, typically near your application's entry point. Business logic modules should never import concrete implementations of their dependencies; they should only know about the Protocol or abstract interface. This separation means you can reconfigure your entire application (swap a database, use a different email provider, switch to an in-memory cache for testing) by changing only the composition root.
Core Concepts
- Constructor injection passes dependencies via
__init__parameters. - Function/method injection passes dependencies as function arguments.
- Protocol-based interfaces define contracts without inheritance, enabling duck-typed DI.
- DI containers (e.g.,
dependency-injector,inject,punq) automate wiring and lifecycle management. - Factory functions and callables act as lightweight providers.
- Scopes control dependency lifecycle: singleton, per-request, transient.
Implementation Patterns
Constructor injection (the simplest pattern)
from typing import Protocol
class EmailSender(Protocol):
def send(self, to: str, subject: str, body: str) -> None: ...
class SmtpEmailSender:
def __init__(self, host: str, port: int):
self.host = host
self.port = port
def send(self, to: str, subject: str, body: str) -> None:
print(f"Sending via SMTP {self.host}:{self.port} to {to}")
class UserService:
def __init__(self, email_sender: EmailSender):
self.email_sender = email_sender
def register(self, email: str, name: str) -> None:
# ... create user ...
self.email_sender.send(
to=email,
subject="Welcome",
body=f"Hello {name}!"
)
# Production wiring
sender = SmtpEmailSender("smtp.example.com", 587)
service = UserService(email_sender=sender)
# Test wiring
class FakeEmailSender:
def __init__(self):
self.sent: list[tuple] = []
def send(self, to, subject, body):
self.sent.append((to, subject, body))
fake = FakeEmailSender()
test_service = UserService(email_sender=fake)
test_service.register("test@example.com", "Test")
assert len(fake.sent) == 1
Function injection with defaults
from typing import Callable
def fetch_data(
url: str,
http_get: Callable[[str], bytes] = None,
) -> dict:
if http_get is None:
import urllib.request
http_get = lambda u: urllib.request.urlopen(u).read()
raw = http_get(url)
return parse(raw)
# In tests
def mock_get(url: str) -> bytes:
return b'{"key": "value"}'
result = fetch_data("https://api.example.com", http_get=mock_get)
Factory pattern for complex construction
from dataclasses import dataclass
from typing import Protocol
class Database(Protocol):
def query(self, sql: str) -> list[dict]: ...
class PostgresDB:
def __init__(self, dsn: str):
self.dsn = dsn
def query(self, sql: str) -> list[dict]:
...
class SQLiteDB:
def __init__(self, path: str):
self.path = path
def query(self, sql: str) -> list[dict]:
...
def create_database(config: dict) -> Database:
if config["type"] == "postgres":
return PostgresDB(dsn=config["dsn"])
elif config["type"] == "sqlite":
return SQLiteDB(path=config["path"])
raise ValueError(f"Unknown database type: {config['type']}")
Lightweight DI container
from typing import Any, TypeVar, Type
T = TypeVar("T")
class Container:
def __init__(self):
self._factories: dict[type, Any] = {}
self._singletons: dict[type, Any] = {}
def register(self, interface: type, factory, singleton: bool = False):
self._factories[interface] = (factory, singleton)
def resolve(self, interface: Type[T]) -> T:
if interface in self._singletons:
return self._singletons[interface]
factory, is_singleton = self._factories[interface]
instance = factory(self)
if is_singleton:
self._singletons[interface] = instance
return instance
# Usage
container = Container()
container.register(EmailSender, lambda c: SmtpEmailSender("smtp.example.com", 587), singleton=True)
container.register(Database, lambda c: PostgresDB("postgresql://localhost/app"), singleton=True)
container.register(UserService, lambda c: UserService(email_sender=c.resolve(EmailSender)))
service = container.resolve(UserService)
FastAPI-style dependency injection
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
app = FastAPI()
def get_db() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_user_repo(db: Session = Depends(get_db)) -> UserRepository:
return UserRepository(db)
@app.get("/users/{user_id}")
def read_user(
user_id: int,
repo: UserRepository = Depends(get_user_repo),
):
return repo.get(user_id)
dependency-injector library
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
email_sender = providers.Singleton(
SmtpEmailSender,
host=config.smtp.host,
port=config.smtp.port,
)
user_service = providers.Factory(
UserService,
email_sender=email_sender,
)
container = Container()
container.config.from_dict({"smtp": {"host": "smtp.example.com", "port": 587}})
service = container.user_service()
Testing with DI: overriding dependencies
import pytest
@pytest.fixture
def fake_sender():
return FakeEmailSender()
@pytest.fixture
def user_service(fake_sender):
return UserService(email_sender=fake_sender)
def test_register_sends_welcome_email(user_service, fake_sender):
user_service.register("alice@example.com", "Alice")
assert len(fake_sender.sent) == 1
assert fake_sender.sent[0][1] == "Welcome"
Best Practices
- Start with constructor injection — it is explicit, requires no framework, and works with any test runner.
- Define dependencies as
Protocoltypes rather than concrete classes to keep coupling loose. - Use factory functions for complex object graphs; reach for a DI container only when manual wiring becomes unwieldy.
- Keep the composition root (where everything is wired together) in one place, typically near the application entry point.
- Prefer
yield-based dependencies (as in FastAPI) for resources that need cleanup. - Make dependencies explicit in function signatures rather than importing globals — this makes testing trivial.
Common Pitfalls
- Service Locator antipattern — injecting the container itself and calling
container.resolve()throughout business logic hides dependencies and makes code harder to understand. - Over-abstracting — not every dependency needs an interface; inject concrete classes when there is only one implementation and testing does not require a fake.
- Circular dependencies — if A depends on B and B depends on A, restructure by extracting a shared interface or mediator.
- Forgetting cleanup — injected resources (DB connections, file handles) still need lifecycle management; use context managers or framework-provided scopes.
- Too many constructor parameters — if a class needs more than 5-6 dependencies, it likely has too many responsibilities; split it.
Anti-Patterns
-
Service locator disguised as DI — passing the entire container into business classes and calling
container.resolve(SomeService)throughout the code. This hides actual dependencies, makes them impossible to discover from the constructor signature, and turns every class into a tightly coupled consumer of the container itself. -
Importing concrete implementations in business logic — writing
from app.services.smtp import SmtpEmailSenderinside a service module instead of depending on anEmailSenderProtocol. This hardcodes the dependency and makes it impossible to swap without modifying the consuming code. -
Interface explosion — creating a Protocol for every single dependency, including trivial helpers and pure functions that will never have an alternative implementation. This adds indirection without value. Only abstract dependencies that genuinely vary between environments (production, testing, staging).
-
Scattered wiring — constructing dependencies in multiple places throughout the codebase rather than centralizing it in a composition root. This makes it impossible to understand what implementation is being used where, and changing a dependency requires hunting through dozens of files.
-
Ignoring dependency lifecycles — treating all dependencies as singletons when some should be per-request (database sessions, HTTP clients with auth tokens) or transient (stateful processors). Mismatched lifecycles cause stale data, connection leaks, and concurrency bugs.
Install this skill directly: skilldb add python-patterns-skills
Related Skills
Async Patterns
Asyncio patterns for concurrent I/O-bound programming in Python
Context Managers
Context manager patterns using with statements for reliable resource management in Python
Dataclasses
Dataclass and Pydantic model patterns for structured data in Python
Decorators
Decorator patterns for wrapping, extending, and composing Python functions and classes
Generators
Generator and itertools patterns for memory-efficient data processing in Python
Metaclasses
Metaclass and descriptor patterns for advanced class customization in Python