TL;DR: Dioxide is a dependency injection framework for Python that makes hexagonal architecture the path of least resistance. v2.0.0 adds constructor-based profile scanning, extensible custom profiles, and introspection APIs.
What is Dioxide?
Dioxide's decorators encode hexagonal architecture patterns directly into your code. @adapter.for_(Port, profile=...) forces you to declare which abstraction you're implementing and when it applies. @service marks core domain logic that depends on ports, not concrete implementations. The result: clean architecture becomes the easiest path forward.
from dioxide import Container, adapter, service, Profile
from typing import Protocol
# Define your port (interface)
class EmailPort(Protocol):
async def send(self, to: str, body: str) -> None: ...
# Production adapter - real infrastructure
@adapter.for_(EmailPort, profile=Profile.PRODUCTION)
class SendGridAdapter:
async def send(self, to: str, body: str) -> None:
# Real SendGrid API call
...
# Test adapter - fast fake
@adapter.for_(EmailPort, profile=Profile.TEST)
class FakeEmailAdapter:
sent: list[dict] = []
async def send(self, to: str, body: str) -> None:
self.sent.append({"to": to, "body": body})
# Service depends on the PORT, not the concrete adapter
@service
class NotificationService:
def __init__(self, email: EmailPort):
self.email = email
# Production: uses SendGrid
async with Container(profile=Profile.PRODUCTION) as container:
svc = container.resolve(NotificationService)
# Tests: uses fast fake, no network calls
async with Container(profile=Profile.TEST) as container:
svc = container.resolve(NotificationService)
What's New in v2.0.0
Auto-Scan via Constructor
Pass profile directly to the Container constructor. The container scans for registered components automatically:
# v2.0: profile in constructor triggers auto-scan
container = Container(profile=Profile.PRODUCTION)
# v1.x: explicit scan still works
container = Container()
container.scan(profile=Profile.PRODUCTION)
Extensible Custom Profiles
Profile changed from StrEnum to an extensible str subclass. Create domain-specific profiles that work identically to built-ins:
LOAD_TEST = Profile("load-test")
INTEGRATION = Profile("integration")
@adapter.for_(DatabasePort, profile=INTEGRATION)
class IntegrationDatabaseAdapter:
...
Container Introspection
Debug container state with list_registered():
container = Container(profile=Profile.TEST)
for registered_type in container.list_registered():
print(f"Registered: {registered_type.__name__}")
Pytest Fixtures
The dioxide.testing module provides isolation helpers:
from dioxide.testing import fresh_container
async def test_my_service():
async with fresh_container(profile=Profile.TEST) as container:
service = container.resolve(MyService)
# Container is fresh, isolated, and auto-cleaned
Breaking Changes
If you used Profile's StrEnum methods, update your code:
| Old (v1.x) | New (v2.0) |
|---|---|
profile.value |
str(profile) |
profile.name |
Use constant directly |
for p in Profile: |
Explicit list |
Why Dioxide?
- Type-safe: Full mypy support catches type mismatches in constructor injection at lint time
- Fast: Rust-backed container with sub-microsecond cached singleton lookup
- Framework integrations: FastAPI, Flask, Django, Celery, Click
- Testing-oriented: Profile system enables fast in-memory fakes instead of mocks
Get Started
pip install dioxide
Top comments (0)