dioxide¶
Clean Architecture Simplified
Declarative dependency injection for Python with type-safe wiring and built-in profiles.
Find What You Need¶
Choose your path based on what you want to accomplish:
15 minutes to your first app
Install dioxide, understand the core concepts, and build a working example with ports, adapters, and services.
Honest comparison & philosophy
See how dioxide compares to dependency-injector, lagom, and other frameworks. Understand the design decisions and limitations.
Fakes over mocks philosophy
Learn why dioxide prefers fakes to mocks, how to structure test adapters, and patterns for fast, deterministic tests.
Framework integration recipes
Copy-paste recipes for FastAPI, testing patterns, configuration, and database integration.
Diagnose and fix common issues
Quick diagnosis table and detailed solutions for adapter not found, circular dependencies, scope errors, and more.
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: auto-scans and activates SendGridAdapter
container = Container(profile=Profile.PRODUCTION)
service = container.resolve(NotificationService)
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.
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 = Container(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 = Container(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
container = Container(profile=Profile.PRODUCTION)
@asynccontextmanager
async def lifespan(app: FastAPI):
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)