Cute DI framework with scopes and agreeable API for Python dependency injection
—
Type annotation markers and dependency key system for clean type-safe dependency injection without polluting business logic code. These markers provide IDE support and runtime type checking while maintaining clean separation of concerns.
Generic type marker for dependency injection that supports both modern and legacy annotation styles.
class FromDishka[T]:
"""
Type marker for dependency injection.
Supports both modern generic syntax and legacy Annotated syntax:
- Modern: FromDishka[Service]
- Legacy: Annotated[Service, FromDishka()]
This marker indicates that the parameter should be injected by Dishka
rather than provided by the caller.
"""
def __init__(self): ...
def __class_getitem__(cls, item: type[T]) -> type[T]:
"""Support for FromDishka[Type] syntax"""Usage Examples:
from dishka import FromDishka
from typing import Annotated
# Modern syntax (Python 3.12+)
def process_data(service: FromDishka[UserService]) -> dict:
return service.get_user_data()
# Legacy syntax (Python 3.10+)
def process_data_legacy(service: Annotated[UserService, FromDishka()]) -> dict:
return service.get_user_data()
# In class constructors
class Controller:
def __init__(
self,
user_service: FromDishka[UserService],
logger: FromDishka[Logger]
):
self.user_service = user_service
self.logger = logger
# With framework integrations
from dishka.integrations.fastapi import inject
@inject
async def api_endpoint(
user_id: int,
service: FromDishka[UserService]
) -> dict:
"""user_id comes from request, service is injected"""
return await service.get_user(user_id)Function for specifying component isolation when injecting dependencies.
class FromComponent:
"""Marker for specifying component when injecting dependencies"""
def __call__(self, component: Component = DEFAULT_COMPONENT) -> FromComponent:
"""
Specify component for dependency resolution.
Parameters:
- component: Component identifier (default: "")
Returns:
FromComponent marker configured for the specified component
"""Usage Examples:
from dishka import FromDishka, FromComponent
from typing import Annotated
# Inject from specific component
def process_with_component(
# Get Database from "primary" component
primary_db: Annotated[Database, FromDishka(), FromComponent("primary")],
# Get Database from "secondary" component
secondary_db: Annotated[Database, FromDishka(), FromComponent("secondary")]
) -> None:
# Use both databases
pass
# In class constructors
class DataSyncService:
def __init__(
self,
source_db: Annotated[Database, FromDishka(), FromComponent("source")],
target_db: Annotated[Database, FromDishka(), FromComponent("target")]
):
self.source_db = source_db
self.target_db = target_db
# Default component (empty string)
def use_default(
db: Annotated[Database, FromDishka(), FromComponent()] # Uses DEFAULT_COMPONENT
) -> None:
passType marker for specifying multiple possible dependency types, allowing flexible dependency resolution.
class AnyOf[*Args]:
"""
Marker for multiple possible dependency types.
Allows specifying that a dependency can be satisfied by any of several types.
Useful for providing fallback implementations or alternative interfaces.
"""Usage Examples:
from dishka import AnyOf, provide
from typing import Protocol
class DatabaseProtocol(Protocol):
def query(self, sql: str) -> list: ...
class FileStorage(Protocol):
def store(self, key: str, data: bytes) -> None: ...
# Provide multiple implementations
@provide(provides=AnyOf[DatabaseProtocol, FileStorage])
class FileBasedDatabase:
def query(self, sql: str) -> list:
# Query implementation
return []
def store(self, key: str, data: bytes) -> None:
# Storage implementation
pass
# Can be injected as either interface
def use_as_database(db: FromDishka[DatabaseProtocol]) -> None:
results = db.query("SELECT * FROM users")
def use_as_storage(storage: FromDishka[FileStorage]) -> None:
storage.store("key", b"data")Generic marker that automatically provides all parent classes and interfaces for a type.
class WithParents[T]:
"""
Automatically provide all parent classes/interfaces for a type.
When used with provide(), registers the implementation for the concrete type
and all of its parent classes and implemented interfaces.
"""Usage Examples:
from dishka import WithParents, provide
from abc import ABC, abstractmethod
class ServiceBase(ABC):
@abstractmethod
def process(self) -> str: ...
class LoggingMixin:
def log(self, message: str) -> None:
print(f"LOG: {message}")
class UserService(ServiceBase, LoggingMixin):
def process(self) -> str:
self.log("Processing user data")
return "processed"
# Automatically provides UserService, ServiceBase, and LoggingMixin
@provide(provides=WithParents[UserService])
def create_user_service() -> UserService:
return UserService()
# Can inject any of the provided types
def use_service(service: FromDishka[UserService]) -> None:
result = service.process()
def use_base(service: FromDishka[ServiceBase]) -> None:
result = service.process()
def use_mixin(logger: FromDishka[LoggingMixin]) -> None:
logger.log("Hello")NamedTuple class that uniquely identifies dependencies by combining type information and component isolation.
class DependencyKey(NamedTuple):
"""Unique identifier for dependencies combining type and component"""
type_hint: Any
"""Type annotation for the dependency"""
component: Component | None
"""Component identifier for isolation (None for default component)"""
def with_component(self, component: Component) -> DependencyKey:
"""
Create a new key with the specified component.
Parameters:
- component: Component identifier
Returns:
New DependencyKey with the same type but different component
"""
def __str__(self) -> str:
"""Human-readable string representation"""Usage Examples:
from dishka import DependencyKey
# Create dependency keys
user_service_key = DependencyKey(UserService, None)
primary_db_key = DependencyKey(Database, "primary")
secondary_db_key = DependencyKey(Database, "secondary")
# Create keys with components
base_key = DependencyKey(Database, None)
primary_key = base_key.with_component("primary")
secondary_key = base_key.with_component("secondary")
# String representation
print(str(user_service_key)) # "UserService"
print(str(primary_db_key)) # "Database[primary]"
print(str(secondary_db_key)) # "Database[secondary]"
# Use in context dictionaries
context = {
DependencyKey(RequestID, None): RequestID("req-123"),
DependencyKey(UserID, None): UserID("user-456"),
}Constants and types for the component system.
Component = str
"""Type alias for component identifiers"""
DEFAULT_COMPONENT: str = ""
"""Default component identifier (empty string)"""Usage Examples:
from dishka import Component, DEFAULT_COMPONENT
# Component identifiers are strings
primary_component: Component = "primary"
secondary_component: Component = "secondary"
cache_component: Component = "cache"
# Default component
default_comp: Component = DEFAULT_COMPONENT # ""
# Use in provider registration
provider = Provider(component="database")
provider.provide(PostgreSQLConnection, scope=Scope.APP)
# Use in dependency resolution
primary_db = container.get(Database, component="primary")
default_db = container.get(Database, component=DEFAULT_COMPONENT)
default_db2 = container.get(Database) # Same as aboveComplex type marker combinations for sophisticated dependency injection scenarios.
Multiple Markers Example:
from typing import Annotated
from dishka import FromDishka, FromComponent
# Combine multiple markers
def complex_injection(
# Primary database from specific component
primary_db: Annotated[
Database,
FromDishka(),
FromComponent("primary")
],
# Service with default component
service: FromDishka[UserService],
# Context variable
request_id: FromDishka[RequestID]
) -> ProcessingResult:
return ProcessingResult(
primary_db.query("SELECT * FROM users"),
service.process_users(),
request_id
)Generic Type Support:
from typing import Generic, TypeVar
T = TypeVar('T')
class Repository(Generic[T]):
def get(self, id: int) -> T | None: ...
def save(self, entity: T) -> T: ...
class User:
id: int
name: str
# Inject generic repositories
def user_operations(
user_repo: FromDishka[Repository[User]]
) -> None:
user = user_repo.get(1)
if user:
user_repo.save(user)Dishka provides extensive type safety through generic types and runtime validation.
Type Checking:
# Type checkers understand FromDishka annotations
def process_user(service: FromDishka[UserService]) -> UserData:
# service is correctly typed as UserService
return service.get_current_user() # IDE autocomplete works
# Invalid usage caught by type checker
def invalid_usage(service: FromDishka[UserService]) -> None:
service.nonexistent_method() # Type errorRuntime Validation:
# Dishka validates types at container creation time
container = make_container(provider) # Validates dependency graph
# Validates at resolution time
user_service = container.get(UserService) # Returns UserService instance
invalid = container.get(NonexistentType) # Raises NoFactoryErrorInstall with Tessl CLI
npx tessl i tessl/pypi-dishka