dioxide¶
Clean Architecture Simplified
Declarative dependency injection for Python with type-safe wiring and built-in profiles.
Why dioxide?¶
Auto-injection via type hints. No manual .bind() or .register() calls. Just decorate and go.
Swap implementations by environment. Production uses real services, tests use fast fakes.
Full mypy and pyright support. If the types check, the wiring is correct.
Quick Start¶
Install dioxide with pip:
pip install dioxide
Define your ports (interfaces), adapters (implementations), and services:
from typing import Protocol
from dioxide import adapter, service, Profile, container
# Define port (interface)
class EmailPort(Protocol):
async def send(self, to: str, subject: str, body: str) -> None: ...
# Production adapter - real email service
@adapter.for_(EmailPort, profile=Profile.PRODUCTION)
class SendGridAdapter:
async def send(self, to: str, subject: str, body: str) -> None:
# Real SendGrid API calls
...
# Test adapter - fast fake for testing
@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})
# Service depends on port, not concrete adapter
@service
class NotificationService:
def __init__(self, email: EmailPort):
self.email = email
async def notify_user(self, user_email: str, message: str):
await self.email.send(user_email, "Notification", message)
# Production: activates SendGridAdapter
container.scan("myapp", profile=Profile.PRODUCTION)
service = container.resolve(NotificationService)
Key Features¶
Explicit @adapter.for_() and @service decorators make your architecture visible. Ports define boundaries, adapters implement them.
@adapter.for_(DatabasePort, profile=Profile.TEST)
class InMemoryDatabase:
...
Different implementations for different environments. No mocking frameworks needed.
# Test uses FakeEmailAdapter automatically
container.scan("app", profile=Profile.TEST)
Opt-in initialization and cleanup with @lifecycle. Resources are managed in dependency order.
@service
@lifecycle
class Database:
async def initialize(self) -> None: ...
async def dispose(self) -> None: ...
Fast container operations via PyO3. Sub-microsecond dependency resolution for production-grade performance.
# Resolution is blazing fast
service = container.resolve(MyService)
Isolate dependencies per request, task, or command. Works for web, CLI, Celery, and any bounded context.
async with container.create_scope() as scope:
ctx = scope.resolve(RequestContext)
Testing Without Mocks¶
dioxide encourages using fast fakes instead of mocking frameworks:
Traditional Approach
# Mocking - tests mock behavior, not real code
@patch('sendgrid.send')
def test_notification(mock_email):
mock_email.return_value = True
# Testing mock behavior...
dioxide Approach
# Fakes - real implementations, real behavior
async def test_notification():
container.scan("app", profile=Profile.TEST)
email = container.resolve(EmailPort)
service = container.resolve(NotificationService)
await service.notify_user("alice@example.com", "Hi!")
assert len(email.sent_emails) == 1
Framework Integration¶
dioxide integrates seamlessly with popular Python frameworks:
from fastapi import FastAPI
from dioxide import container, Profile
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
container.scan("myapp", profile=Profile.PRODUCTION)
async with container:
yield
app = FastAPI(lifespan=lifespan)
@app.post("/users")
async def create_user(user: UserData):
service = container.resolve(UserService)
await service.register_user(user.email, user.name)