Dependency Injection (DI) is a design pattern where dependencies are provided (injected) from outside rather than created inside a class.
Without DI, classes create their own dependencies, leading to tight coupling:
class UserService:
def __init__(self) -> None:
self.email = EmailService() # ❌ Tight coupling
def register_user(self, email: str) -> None:
self.email.send_email(email, "Welcome!")Issues: Hard to test, can't swap implementations, hidden dependencies.
With DI, dependencies are injected from outside:
class UserService:
def __init__(self, email: EmailSender) -> None: # ✅ Injected
self.email = email
def register_user(self, email: str) -> None:
self.email.send_email(email, "Welcome!")Benefits: Easy testing, loose coupling, explicit dependencies.
Inject mocks for testing:
def test_user_service() -> None:
mock_email = Mock(spec=EmailSender)
service = UserService(email=mock_email)
service.register_user("test@example.com")
mock_email.send_email.assert_called_once()Depend on abstractions, not implementations:
class UserService:
def __init__(self, cache: CacheBackend) -> None:
self.cache = cache
# Swap implementations easily
service = UserService(cache=RedisCache()) # Production
service = UserService(cache=DictCache()) # Development
service = UserService(cache=MockCache()) # TestingObjects can have different lifetime cycles (singleton, scoped, transient).
Here are examples in modern-di:
from modern_di import Group, Scope, providers
class AppModule(Group):
# Singleton: one instance for entire app
config = providers.Factory(
scope=Scope.APP,
creator=AppConfig,
cache_settings=providers.CacheSettings()
)
# Scoped: one instance per request
db_session = providers.Factory(
scope=Scope.REQUEST,
creator=DatabaseSession,
cache_settings=providers.CacheSettings()
)
# Transient: new instance each time
request_id = providers.Factory(
scope=Scope.REQUEST,
creator=lambda: str(uuid.uuid4())
)config = AppConfig()
db = DatabaseConnection(config)
email = EmailService(config)
user_service = UserService(db, email)Problems: Unwieldy at scale, no lifetime management, scattered configuration.
import dataclasses
from modern_di import Container, Group, Scope, providers
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class AppConfig:
db_host: str = "localhost"
db_port: int = 5432
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class DatabaseConnection:
config: AppConfig # ✅ Auto-injected from type hint
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class UserService:
db: DatabaseConnection # ✅ Auto-injected
# Declare dependencies
class AppModule(Group):
config = providers.Factory(
scope=Scope.APP,
creator=AppConfig,
cache_settings=providers.CacheSettings()
)
db = providers.Factory(scope=Scope.REQUEST, creator=DatabaseConnection)
user_service = providers.Factory(scope=Scope.REQUEST, creator=UserService)
# Resolve entire dependency graph
container = Container(groups=[AppModule])
user_service = container.resolve(UserService)Type annotations auto-wire dependencies - no manual registration needed.
Hierarchical containers with automatic inheritance:
app_container = Container(groups=[AppModule], scope=Scope.APP)
request_container = app_container.build_child_container(scope=Scope.REQUEST)
# Resolves from correct scope automatically
db_pool = request_container.resolve(DatabasePool) # APP scope
db_session = request_container.resolve(DatabaseSession) # REQUEST scopeOverride any dependency:
@pytest.fixture
def test_container() -> Container:
container = Container(groups=[AppModule])
container.override(AppModule.db, Mock(spec=DatabaseConnection))
return containerDefine finalizers for automatic cleanup:
class AppModule(Group):
db_session = providers.Factory(
scope=Scope.REQUEST,
creator=DatabaseSession,
cache_settings=providers.CacheSettings(
finalizer=lambda session: session.close() # ✅ Define cleanup
)
)
# Automatic finalizer execution
request_container = app_container.build_child_container(scope=Scope.REQUEST)
try:
session = request_container.resolve(DatabaseSession)
finally:
request_container.close_sync() # Finalizers calledWorks with FastAPI, LiteStar, FastStream:
from modern_di_fastapi import FromDI
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: UserService = FromDI(UserService),
) -> dict:
return {"user": user_service.get_user(user_id)}Dependency Injection:
- Decouples classes from dependencies
- Improves testability with easy mocking
- Centralizes configuration
- Manages lifetimes automatically
modern-di automates DI with minimal boilerplate through type annotations, providing scope management, testing utilities, and framework integrations.