dioxide

Clean Architecture Simplified

Declarative dependency injection for Python with type-safe wiring and built-in profiles.

Python 3.11+ License: MIT PyPI version


Why dioxide?


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

Hexagonal Architecture

Explicit @adapter.for_() and @service decorators make your architecture visible. Ports define boundaries, adapters implement them.

@adapter.for_(DatabasePort, profile=Profile.TEST)
class InMemoryDatabase:
    ...
Profile-Based Testing

Different implementations for different environments. No mocking frameworks needed.

# Test uses FakeEmailAdapter automatically
container.scan("app", profile=Profile.TEST)
Lifecycle Management

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: ...
Rust Performance

Fast container operations via PyO3. Sub-microsecond dependency resolution for production-grade performance.

# Resolution is blazing fast
service = container.resolve(MyService)
Request Scoping

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

Learn more about testing with fakes


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)

See the Getting Started guide


Ready to Get Started?