dioxide

Clean Architecture Simplified

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

Python 3.11+ License: MIT PyPI version


Find What You Need

Choose your path based on what you want to accomplish:

Just want to get started?

15 minutes to your first app

Install dioxide, understand the core concepts, and build a working example with ports, adapters, and services.

Getting Started
Evaluating dioxide for your project?

Honest comparison & philosophy

See how dioxide compares to dependency-injector, lagom, and other frameworks. Understand the design decisions and limitations.

Why dioxide?
Need testing patterns?

Fakes over mocks philosophy

Learn why dioxide prefers fakes to mocks, how to structure test adapters, and patterns for fast, deterministic tests.

Dioxide Testing Guide: Fakes Over Mocks
Integrating with a framework?

Framework integration recipes

Copy-paste recipes for FastAPI, testing patterns, configuration, and database integration.

Cookbook
Hit an error?

Diagnose and fix common issues

Quick diagnosis table and detailed solutions for adapter not found, circular dependencies, scope errors, and more.

Troubleshooting

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?


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 = Container(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 = 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

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

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)

See the FastAPI cookbook


Ready to Get Started?