Migrating from dependency-injector¶
Target Audience: Python developers currently using dependency-injector who want a simpler, faster DI solution.
Time to Migrate: 1-2 hours for small projects, half a day for medium projects.
Why Migrate?¶
If you’re using dependency-injector and experiencing any of these pain points, dioxide might be a better fit:
Wiring Ceremony¶
dependency-injector requires a 3-step ceremony for every injection:
# dependency-injector: 3 steps for every injection
from dependency_injector.wiring import inject, Provide
# Step 1: Import inject
# Step 2: Import Provide
# Step 3: Use @inject decorator with Provide[] annotation
@inject
def get_user(user_service: UserService = Provide[Container.user_service]):
return user_service.get_current_user()
# Step 4: Don't forget to call wire()!
container.wire(modules=[__name__])
dioxide eliminates this entirely:
# dioxide: Just type hints
from dioxide import container
def get_user():
user_service = container.resolve(UserService)
return user_service.get_current_user()
# Or with constructor injection (no decorators needed)
@service
class UserController:
def __init__(self, user_service: UserService): # Auto-injected!
self.user_service = user_service
Nested Container Confusion¶
dependency-injector’s nested containers are a common source of bugs:
Issue #936: Singleton scope behavior in nested containers
Issue #937: Container inheritance resolution order
Issue #841: Overriding providers in nested containers
Issue #912: Nested container lifecycle management
dioxide uses a simple flat container with profiles for environment-based configuration:
# dioxide: Simple profile-based configuration
container.scan(profile=Profile.PRODUCTION) # Use production adapters
container.scan(profile=Profile.TEST) # Use test fakes
Performance Under Load¶
dependency-injector has reported performance issues under load:
Issue #904: 400ms+ per request in high-concurrency scenarios
dioxide’s Rust-backed container provides consistent sub-microsecond resolution:
# dioxide benchmark results (from real benchmarks)
# Resolution: 167-300ns per resolve
# 1000x faster than Python-based containers
Framework Integration Bugs¶
dependency-injector has ongoing issues with popular frameworks:
Issue #938: FastAPI Depends integration
Issue #908: FastAPI lifespan context
Issue #712: Django settings integration
dioxide integrates cleanly with framework patterns:
# dioxide + FastAPI: Clean integration
from fastapi import FastAPI
from dioxide import container, Profile
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
container.scan(profile=Profile.PRODUCTION)
async with container:
yield
app = FastAPI(lifespan=lifespan)
Async Resource Lifecycle¶
dependency-injector’s Resource provider has issues with async cleanup:
Issue #567: Async resource shutdown not awaited
dioxide’s @lifecycle decorator properly handles async initialization and disposal:
# dioxide: Proper async lifecycle
from dioxide import adapter, lifecycle, Profile
@adapter.for_(DatabasePort, profile=Profile.PRODUCTION)
@lifecycle
class PostgresAdapter:
async def initialize(self) -> None:
self.engine = create_async_engine(...)
await self.engine.connect()
async def dispose(self) -> None:
await self.engine.dispose() # Properly awaited!
# Lifecycle is automatic with async context manager
async with container:
# initialize() called on all @lifecycle components
db = container.resolve(DatabasePort)
# dispose() properly awaited on all @lifecycle components
Type Safety Holes¶
dependency-injector’s __getattr__ on containers silences type errors:
Issue #910: Type checkers can’t detect missing providers
dioxide is fully type-safe:
# dependency-injector: No type error (but will fail at runtime!)
class Container(containers.DeclarativeContainer):
pass
container = Container()
service = container.user_service() # No error from mypy, fails at runtime!
# dioxide: Type-safe resolution
service = container.resolve(UserService) # mypy validates UserService type
Concept Mapping¶
Here’s how dependency-injector concepts map to dioxide:
dependency-injector |
dioxide |
Notes |
|---|---|---|
|
|
dioxide containers are simpler |
|
|
Default scope is singleton |
|
|
Use Scope.FACTORY for new instances |
|
Pydantic Settings + |
Use Pydantic for config validation |
|
|
Proper async support |
|
Type hints only |
No decorators needed |
|
|
Auto-discovery based on decorators |
|
Profile system |
|
Nested containers |
Single container + profiles |
Simpler mental model |
Side-by-Side Examples¶
Let’s convert common dependency-injector patterns to dioxide.
Basic Service with Dependency¶
dependency-injector:
# containers.py
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
database = providers.Singleton(
Database,
host=config.db.host,
port=config.db.port,
)
user_repository = providers.Singleton(
UserRepository,
database=database,
)
user_service = providers.Singleton(
UserService,
repository=user_repository,
)
# main.py
from dependency_injector.wiring import inject, Provide
@inject
def main(user_service: UserService = Provide[Container.user_service]):
user = user_service.get_user(1)
print(user)
if __name__ == "__main__":
container = Container()
container.config.from_yaml("config.yml")
container.wire(modules=[__name__])
main()
dioxide:
# services.py
from typing import Protocol
from dioxide import adapter, service, Profile
# Define ports (interfaces)
class DatabasePort(Protocol):
async def query(self, sql: str) -> list[dict]: ...
class UserRepositoryPort(Protocol):
async def get_user(self, user_id: int) -> dict | None: ...
# Define adapters (implementations)
@adapter.for_(DatabasePort, profile=Profile.PRODUCTION)
class PostgresDatabase:
def __init__(self, config: AppConfig):
self.host = config.db_host
self.port = config.db_port
async def query(self, sql: str) -> list[dict]:
# Real database query
pass
@adapter.for_(UserRepositoryPort, profile=Profile.PRODUCTION)
class PostgresUserRepository:
def __init__(self, db: DatabasePort):
self.db = db
async def get_user(self, user_id: int) -> dict | None:
results = await self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
return results[0] if results else None
# Define service (business logic)
@service
class UserService:
def __init__(self, repository: UserRepositoryPort):
self.repository = repository
async def get_user(self, user_id: int) -> dict | None:
return await self.repository.get_user(user_id)
# main.py
from dioxide import Container, Profile
from pydantic_settings import BaseSettings
class AppConfig(BaseSettings):
db_host: str = "localhost"
db_port: int = 5432
model_config = {"env_prefix": "APP_"}
async def main():
container = Container()
container.register_instance(AppConfig, AppConfig())
container.scan(profile=Profile.PRODUCTION)
user_service = container.resolve(UserService)
user = await user_service.get_user(1)
print(user)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Configuration Handling¶
dependency-injector:
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
api_client = providers.Singleton(
ApiClient,
base_url=config.api.base_url,
api_key=config.api.api_key,
timeout=config.api.timeout,
)
# Usage
container = Container()
container.config.from_yaml("config.yml")
container.config.api.api_key.from_env("API_KEY")
dioxide (with Pydantic Settings):
from pydantic_settings import BaseSettings
from dioxide import Container, adapter, Profile
# Type-safe, validated configuration
class ApiConfig(BaseSettings):
base_url: str = "https://api.example.com"
api_key: str # Required, from API_KEY env var
timeout: int = 30
model_config = {"env_prefix": "API_"}
class ApiClientPort(Protocol):
async def request(self, endpoint: str) -> dict: ...
@adapter.for_(ApiClientPort, profile=Profile.PRODUCTION)
class RealApiClient:
def __init__(self, config: ApiConfig):
self.base_url = config.base_url
self.api_key = config.api_key
self.timeout = config.timeout
async def request(self, endpoint: str) -> dict:
# Real API call
pass
# Usage
container = Container()
container.register_instance(ApiConfig, ApiConfig()) # Loads from env vars
container.scan(profile=Profile.PRODUCTION)
client = container.resolve(ApiClientPort)
Testing Setup¶
dependency-injector:
# test_user_service.py
from unittest.mock import Mock
from dependency_injector import providers
def test_user_service():
# Create container
container = Container()
# Override with mocks
mock_repository = Mock()
mock_repository.get_user.return_value = {"id": 1, "name": "Alice"}
container.user_repository.override(providers.Object(mock_repository))
# Wire and test
container.wire(modules=[__name__])
service = container.user_service()
user = service.get_user(1)
assert user["name"] == "Alice"
mock_repository.get_user.assert_called_once_with(1)
# Don't forget to reset!
container.user_repository.reset_override()
dioxide (with fakes, no mocks):
# adapters/fakes.py - Fakes live in production code!
from dioxide import adapter, Profile
@adapter.for_(DatabasePort, profile=Profile.TEST)
class FakeDatabase:
def __init__(self):
self.data = {}
async def query(self, sql: str) -> list[dict]:
# Simple in-memory implementation
return list(self.data.values())
@adapter.for_(UserRepositoryPort, profile=Profile.TEST)
class FakeUserRepository:
def __init__(self):
self.users = {}
async def get_user(self, user_id: int) -> dict | None:
return self.users.get(user_id)
# Test helper (not in protocol)
def seed(self, **users):
self.users.update(users)
# test_user_service.py
import pytest
from dioxide import Container, Profile
@pytest.fixture
def container():
c = Container()
c.scan(profile=Profile.TEST) # Activates fakes!
return c
async def test_user_service(container):
# Seed test data
fake_repo = container.resolve(UserRepositoryPort)
fake_repo.seed(**{1: {"id": 1, "name": "Alice"}})
# Test REAL service with REAL fakes
service = container.resolve(UserService)
user = await service.get_user(1)
# Assert observable outcomes (no mock.assert_called!)
assert user["name"] == "Alice"
FastAPI Integration¶
dependency-injector:
# containers.py
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(modules=["api"])
user_service = providers.Singleton(UserService)
# api.py
from fastapi import FastAPI, Depends
from dependency_injector.wiring import inject, Provide
app = FastAPI()
@app.get("/users/{user_id}")
@inject
async def get_user(
user_id: int,
user_service: UserService = Depends(Provide[Container.user_service]),
):
return user_service.get_user(user_id)
# main.py
container = Container()
container.wire(modules=["api"])
app.container = container
dioxide:
# main.py
from fastapi import FastAPI
from dioxide import container, Profile
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
container.scan(profile=Profile.PRODUCTION)
async with container:
yield
app = FastAPI(lifespan=lifespan)
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user_service = container.resolve(UserService)
return await user_service.get_user(user_id)
# Or create a dependency for cleaner routes
def get_user_service() -> UserService:
return container.resolve(UserService)
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
user_service: UserService = Depends(get_user_service),
):
return await user_service.get_user(user_id)
Step-by-Step Migration¶
Follow these steps to migrate your project from dependency-injector to dioxide.
Step 1: Install dioxide¶
# Add dioxide
pip install dioxide
# or
uv add dioxide
# Keep dependency-injector temporarily for gradual migration
# Remove it after migration is complete
Step 2: Define Ports (Interfaces)¶
For each provider in your dependency-injector container, create a Protocol:
# Before: No explicit interface
class UserService:
def __init__(self, repository):
self.repository = repository
# After: Explicit port
from typing import Protocol
class UserRepositoryPort(Protocol):
async def get_user(self, user_id: int) -> dict | None: ...
async def create_user(self, name: str, email: str) -> dict: ...
class UserServicePort(Protocol):
async def get_user(self, user_id: int) -> dict | None: ...
async def register_user(self, name: str, email: str) -> dict: ...
Step 3: Convert Singletons to @service or @adapter¶
Singletons that are business logic become @service:
# Before: dependency-injector Singleton
class Container(containers.DeclarativeContainer):
user_service = providers.Singleton(UserService, repository=user_repository)
# After: dioxide @service
from dioxide import service
@service
class UserService:
def __init__(self, repository: UserRepositoryPort):
self.repository = repository
Singletons that are infrastructure become @adapter:
# Before: dependency-injector Singleton
class Container(containers.DeclarativeContainer):
database = providers.Singleton(PostgresDatabase, config=config)
# After: dioxide @adapter
from dioxide import adapter, Profile
@adapter.for_(DatabasePort, profile=Profile.PRODUCTION)
class PostgresDatabase:
def __init__(self, config: DatabaseConfig):
self.config = config
Step 4: Convert Configuration¶
Before (dependency-injector):
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
container.config.from_yaml("config.yml")
container.config.db.host.from_env("DB_HOST")
After (Pydantic Settings):
from pydantic_settings import BaseSettings
class DatabaseConfig(BaseSettings):
host: str = "localhost"
port: int = 5432
username: str = "postgres"
password: str
model_config = {"env_prefix": "DB_"}
# Register as instance
container.register_instance(DatabaseConfig, DatabaseConfig())
Step 5: Convert Resources to @lifecycle¶
Before (dependency-injector):
class Container(containers.DeclarativeContainer):
db_engine = providers.Resource(
init_db_engine,
config=config.db,
)
def init_db_engine(config):
engine = create_engine(config.url)
yield engine
engine.dispose()
After (dioxide @lifecycle):
from dioxide import adapter, lifecycle, Profile
@adapter.for_(DatabasePort, profile=Profile.PRODUCTION)
@lifecycle
class PostgresAdapter:
def __init__(self, config: DatabaseConfig):
self.config = config
self.engine = None
async def initialize(self) -> None:
self.engine = create_async_engine(self.config.url)
async def dispose(self) -> None:
if self.engine:
await self.engine.dispose()
Step 6: Convert Overrides to Profiles¶
Before (dependency-injector overrides):
# Production
container = Container()
# Testing - override with mocks
container.database.override(providers.Object(mock_db))
container.user_repository.override(providers.Object(mock_repo))
After (dioxide profiles):
# Production adapters
@adapter.for_(DatabasePort, profile=Profile.PRODUCTION)
class PostgresDatabase:
pass
# Test fakes (real implementations, not mocks)
@adapter.for_(DatabasePort, profile=Profile.TEST)
class FakeDatabase:
def __init__(self):
self.data = {}
# Production
container.scan(profile=Profile.PRODUCTION)
# Testing
container.scan(profile=Profile.TEST)
Step 7: Remove Wiring¶
Before:
from dependency_injector.wiring import inject, Provide
@inject
def get_user(service: UserService = Provide[Container.user_service]):
return service.get_user(1)
container.wire(modules=[__name__])
After:
# No decorators, no wiring!
def get_user():
service = container.resolve(UserService)
return service.get_user(1)
Step 8: Update Tests¶
Before:
def test_user_service():
container = Container()
container.user_repository.override(providers.Object(mock_repo))
container.wire(modules=[__name__])
mock_repo.get_user.return_value = {"id": 1}
service = container.user_service()
result = service.get_user(1)
mock_repo.get_user.assert_called_once_with(1)
container.user_repository.reset_override()
After:
async def test_user_service(container):
# Seed fake with test data
fake_repo = container.resolve(UserRepositoryPort)
fake_repo.seed(**{1: {"id": 1, "name": "Alice"}})
# Test real behavior
service = container.resolve(UserService)
result = await service.get_user(1)
assert result["id"] == 1
Step 9: Remove dependency-injector¶
Once migration is complete:
pip uninstall dependency-injector
# or
uv remove dependency-injector
Common Patterns¶
Singletons vs Factories¶
dependency-injector:
class Container(containers.DeclarativeContainer):
# Singleton - one instance
db_connection = providers.Singleton(DatabaseConnection)
# Factory - new instance each time
request_handler = providers.Factory(RequestHandler)
dioxide:
from dioxide import adapter, Profile, Scope
# Singleton (default) - one instance
@adapter.for_(DatabasePort, profile=Profile.PRODUCTION)
class PostgresConnection:
pass
# Factory - new instance each time
@adapter.for_(RequestHandlerPort, profile=Profile.PRODUCTION, scope=Scope.FACTORY)
class RequestHandler:
pass
Configuration with Pydantic¶
Replace dependency-injector’s Configuration provider with Pydantic Settings for type-safe, validated configuration:
from pydantic_settings import BaseSettings
from pydantic import Field
class AppConfig(BaseSettings):
# Database settings
db_host: str = "localhost"
db_port: int = 5432
db_name: str = "app"
db_password: str = Field(..., env="DB_PASSWORD") # Required
# API settings
api_key: str = Field(..., env="API_KEY")
api_timeout: int = 30
# Feature flags
debug: bool = False
model_config = {
"env_prefix": "APP_",
"env_file": ".env",
}
# Usage
config = AppConfig() # Loads from environment
container.register_instance(AppConfig, config)
Testing Without Mocks¶
dioxide encourages fakes over mocks for clearer, more maintainable tests:
# Define test fakes alongside production adapters
@adapter.for_(EmailPort, profile=Profile.TEST)
class FakeEmailAdapter:
def __init__(self):
self.sent_emails = []
async def send(self, to: str, subject: str, body: str) -> None:
self.sent_emails.append({"to": to, "subject": subject, "body": body})
# Test helpers
def verify_sent_to(self, email: str) -> bool:
return any(e["to"] == email for e in self.sent_emails)
def clear(self):
self.sent_emails = []
# Tests use real fakes
async def test_sends_welcome_email(container):
fake_email = container.resolve(EmailPort)
service = container.resolve(UserService)
await service.register_user("alice@example.com", "Alice")
assert fake_email.verify_sent_to("alice@example.com")
Async Lifecycle¶
dioxide properly handles async resource initialization and cleanup:
from dioxide import adapter, lifecycle, Profile
@adapter.for_(CachePort, profile=Profile.PRODUCTION)
@lifecycle
class RedisCache:
def __init__(self, config: CacheConfig):
self.config = config
self.redis = None
async def initialize(self) -> None:
import aioredis
self.redis = await aioredis.create_redis_pool(
self.config.redis_url,
minsize=5,
maxsize=10,
)
async def dispose(self) -> None:
if self.redis:
self.redis.close()
await self.redis.wait_closed()
async def get(self, key: str) -> str | None:
return await self.redis.get(key)
async def set(self, key: str, value: str, ttl: int = 3600) -> None:
await self.redis.setex(key, ttl, value)
# Lifecycle is automatic
async with container:
# RedisCache.initialize() called here
cache = container.resolve(CachePort)
await cache.set("key", "value")
# RedisCache.dispose() called here (properly awaited!)
FAQ¶
How do I handle providers that need runtime arguments?¶
dependency-injector:
class Container(containers.DeclarativeContainer):
user_service = providers.Factory(UserService, repository=repository)
service = container.user_service(extra_arg="value")
dioxide:
For runtime arguments, use explicit construction or factory functions:
# Option 1: Resolve dependencies, construct manually
repository = container.resolve(UserRepositoryPort)
service = UserService(repository=repository, extra_arg="value")
# Option 2: Register a factory
container.register_factory(
UserServiceWithArg,
lambda: UserService(
repository=container.resolve(UserRepositoryPort),
extra_arg="runtime_value",
)
)
How do I migrate nested containers?¶
Flatten nested containers into a single container with profiles:
Before:
class CoreContainer(containers.DeclarativeContainer):
database = providers.Singleton(Database)
class AppContainer(containers.DeclarativeContainer):
core = providers.Container(CoreContainer)
user_service = providers.Singleton(UserService, db=core.database)
After:
# Single flat container with all components
@adapter.for_(DatabasePort, profile=Profile.PRODUCTION)
class PostgresDatabase:
pass
@service
class UserService:
def __init__(self, db: DatabasePort):
self.db = db
# One container, one scan
container.scan(profile=Profile.PRODUCTION)
How do I handle circular dependencies?¶
dioxide detects circular dependencies and raises a clear error. Refactor to break the cycle:
# Circular: A depends on B, B depends on A
# Solution: Introduce an interface or event system
class EventBusPort(Protocol):
def publish(self, event: Event) -> None: ...
def subscribe(self, handler: Callable) -> None: ...
@service
class ServiceA:
def __init__(self, event_bus: EventBusPort):
self.event_bus = event_bus
self.event_bus.subscribe(self.handle_b_event)
@service
class ServiceB:
def __init__(self, event_bus: EventBusPort):
self.event_bus = event_bus
self.event_bus.subscribe(self.handle_a_event)
Can I migrate incrementally?¶
Yes! You can use both frameworks during migration:
# Keep dependency-injector for unmigrated code
from dependency_injector import containers, providers
# Start using dioxide for new code
from dioxide import container, service, adapter, Profile
# Gradually move providers from dependency-injector to dioxide
How does dioxide compare in performance?¶
dioxide is significantly faster due to its Rust backend:
Operation |
dependency-injector |
dioxide |
|---|---|---|
Simple resolution |
~10-50us |
~167-300ns |
Nested dependencies |
~50-200us |
~300-500ns |
High concurrency |
Degrades (issue #904) |
Consistent |
What if I need features dioxide doesn’t have?¶
dioxide is intentionally focused on hexagonal architecture patterns. If you need features like:
Coroutine providers: Use
@lifecyclewith async initialization insteadCallable providers: Use
container.register_factory()Aggregate providers: Compose manually or use service aggregation
dioxide prioritizes simplicity over feature count.
Migration Checklist¶
Use this checklist to track your migration progress:
[ ] Install dioxide
[ ] Define ports (Protocol) for each abstraction
[ ] Convert business logic Singletons to @service
[ ] Convert infrastructure Singletons to @adapter
[ ] Convert Factory providers to Scope.FACTORY adapters
[ ] Replace Configuration with Pydantic Settings
[ ] Convert Resource providers to @lifecycle
[ ] Replace container overrides with Profile system
[ ] Remove @inject decorators and Provide[] annotations
[ ] Remove container.wire() calls
[ ] Create test fakes for each port
[ ] Update test fixtures to use Profile.TEST
[ ] Replace mock assertions with fake state checks
[ ] Update FastAPI/Flask integration
[ ] Remove dependency-injector package
[ ] Run full test suite
[ ] Update documentation
Getting Help¶
If you encounter issues during migration:
GitHub Issues: https://github.com/mikelane/dioxide/issues
Discussions: https://github.com/mikelane/dioxide/discussions
Documentation: https://dioxide.readthedocs.io
When reporting migration issues, include:
Your dependency-injector pattern/code
Your attempted dioxide conversion
The error or unexpected behavior
We’re happy to help with migration questions!
See also
Getting Started - Complete introduction to dioxide
Testing with Fakes - Testing philosophy and patterns
Hexagonal Architecture with dioxide - Ports and adapters in depth