https://github.com/mikelane/dioxide
Opinionated, Zero-ceremony, Rust-backed dependency injection for Python
https://github.com/mikelane/dioxide
clean-architecture clean-code dependency-injection dependency-inversion-principle hexagonal-architecture python python311 python312 python313 python314 software-engineering
Last synced: 15 days ago
JSON representation
Opinionated, Zero-ceremony, Rust-backed dependency injection for Python
- Host: GitHub
- URL: https://github.com/mikelane/dioxide
- Owner: mikelane
- License: mit
- Created: 2025-10-21T21:42:00.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-01-21T15:09:23.000Z (20 days ago)
- Last Synced: 2026-01-21T17:39:45.719Z (20 days ago)
- Topics: clean-architecture, clean-code, dependency-injection, dependency-inversion-principle, hexagonal-architecture, python, python311, python312, python313, python314, software-engineering
- Language: Python
- Homepage: https://dioxide.readthedocs.io
- Size: 2.21 MB
- Stars: 8
- Watchers: 0
- Forks: 0
- Open Issues: 20
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Codeowners: .github/CODEOWNERS
- Roadmap: ROADMAP_OLD.md
Awesome Lists containing this project
README
DI that makes hexagonal architecture feel inevitable
[](https://github.com/mikelane/dioxide/actions)
[](https://github.com/mikelane/dioxide/actions/workflows/release-automated.yml)
[](https://pypi.org/project/dioxide/)
[](https://dioxide.readthedocs.io/en/latest/?badge=latest)
[](https://pypi.org/project/dioxide/)
[](https://github.com/mikelane/dioxide)
[](https://github.com/mikelane/dioxide)
[](https://opensource.org/licenses/MIT)
[](https://github.com/sponsors/mikelane)
> **[π Read the Documentation](https://dioxide.readthedocs.io)** | **[π Quick Start](#quick-start)** | **[π‘ Examples](https://dioxide.readthedocs.io/en/latest/examples/)** | **[π API Reference](https://dioxide.readthedocs.io/en/latest/api/dioxide/)**
---
## Overview
`dioxide` is a dependency injection framework for Python designed to make clean architecture the path of least resistance:
- **Zero-ceremony API** - Just type hints and decorators, no XML or configuration files
- **Built-in Profile system** - Swap PostgreSQL for in-memory with `profile=Profile.TEST`
- **Hexagonal architecture made easy** - `@adapter.for_(Port)` and `@service` guide you to clean code
- **Type safety** - Full mypy and pyright support; if it type-checks, it wires correctly
- **Rust-backed** - Fast container operations via PyO3 for competitive runtime performance
## Installation
```bash
pip install dioxide
```
### Platform Support
| Platform | x86_64 | ARM64/aarch64 |
|----------|--------|---------------|
| Linux | β
| β
|
| macOS | β
| β
(M1/M2/M3) |
| Windows | β
| β |
**Python Versions**: 3.11, 3.12, 3.13, 3.14
## Status
**STABLE**: dioxide v1.0.0 is production-ready with a stable, frozen API.
- **Latest Release**: [v1.0.0](https://github.com/mikelane/dioxide/releases/tag/v1.0.0) - Published to PyPI
- **API Status**: Frozen - No breaking changes until v2.0
- **Production Ready**: All MLP features complete, comprehensive test coverage, battle-tested
**Migrating from alpha/beta versions?** See [MIGRATION.md](MIGRATION.md) for the complete migration guide.
See [Issues](https://github.com/mikelane/dioxide/issues) for current work and planned enhancements.
## Why dioxide?
**Make the Dependency Inversion Principle feel inevitable.**
dioxide exists to make clean architecture (ports-and-adapters) the path of least resistance:
| What you get | How dioxide helps |
|--------------|-------------------|
| **Testable code** | Profile system swaps real adapters for fakes with one line |
| **Type-safe wiring** | If mypy passes, your dependencies are correct |
| **Clean boundaries** | `@adapter.for_(Port)` makes seams explicit and visible |
| **Fast tests** | In-memory fakes, not slow mocks or external services |
| **Competitive performance** | Rust-backed container with sub-microsecond resolution |
See [MLP_VISION.md](docs/MLP_VISION.md) for the complete design philosophy and [TESTING_GUIDE.md](docs/TESTING_GUIDE.md) for testing patterns.
## Anti-goals
dioxide is intentionally focused. Here's what we're **not** building:
- **Not a framework** - dioxide is a library; it doesn't control your application structure
- **Not runtime reflection magic** - Everything is explicit via decorators and type hints
- **Not a general-purpose container** - We optimize for hexagonal architecture patterns
- **Not configuration management** - Use Pydantic Settings or python-decouple for that
- **Not trying to solve every DI pattern** - Constructor injection only, no property/method injection
This focus keeps dioxide simple and predictable. See [MLP_VISION.md](docs/MLP_VISION.md) for the complete design philosophy.
## Quick Start
dioxide embraces **hexagonal architecture** (ports-and-adapters) to make clean, testable code the path of least resistance.
```python
from typing import Protocol
from dioxide import Container, Profile, adapter, service
# 1. Define port (interface) - your seam
class EmailPort(Protocol):
async def send(self, to: str, subject: str, body: str) -> None: ...
# 2. Create adapters (implementations) for different environments
@adapter.for_(EmailPort, profile=Profile.PRODUCTION)
class SendGridAdapter:
async def send(self, to: str, subject: str, body: str) -> None:
# Real SendGrid API calls
print(f"π§ Sending via SendGrid to {to}: {subject}")
@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})
print(f"β
Fake email sent to {to}")
# 3. Create service (core business logic) - depends on port, not adapter
@service
class UserService:
def __init__(self, email: EmailPort):
self.email = email
async def register_user(self, email_addr: str, name: str):
# Core logic doesn't know/care which adapter is active
await self.email.send(
to=email_addr,
subject="Welcome!",
body=f"Hello {name}, thanks for signing up!"
)
# Production usage
container = Container()
container.scan(profile=Profile.PRODUCTION)
user_service = container.resolve(UserService)
await user_service.register_user("user@example.com", "Alice")
# π§ Sends real email via SendGrid
# Testing - just change the profile!
test_container = Container()
test_container.scan(profile=Profile.TEST)
test_service = test_container.resolve(UserService)
await test_service.register_user("test@example.com", "Bob")
# Verify in tests (no mocks!)
fake_email = test_container.resolve(EmailPort)
assert len(fake_email.sent_emails) == 1
assert fake_email.sent_emails[0]["to"] == "test@example.com"
```
**Why this is powerful**:
- β
**Type-safe**: If mypy passes, your wiring is correct
- β
**Testable**: Fast fakes at the seams, not mocks
- β
**Clean**: Business logic has zero knowledge of infrastructure
- β
**Simple**: One line change to swap implementations (`profile=...`)
- β
**Explicit**: Port definitions make boundaries visible
**Key concepts**:
- **Ports** (`Protocol`): Define what operations you need (the seam)
- **Adapters** (`@adapter.for_(Port, profile=...)`): Concrete implementations
- **Services** (`@service`): Core business logic that depends on ports
- **Profiles** (`Profile.PRODUCTION`, `Profile.TEST`): Environment selection
- **Container**: Auto-wires dependencies based on type hints
## Constructor Dependency Injection
When you create an adapter or service with constructor parameters, dioxide **automatically injects dependencies** based on type hints. This is the core mechanism that makes dependency injection "just work".
### How It Works
When you write:
```python
@adapter.for_(UserRepository, profile=Profile.PRODUCTION)
class SqliteUserRepository:
def __init__(self, db: Connection) -> None: # <-- type hint tells dioxide what to inject
self.db = db
```
You might ask: "How does dioxide know where to get `db`?"
The answer: **The dependency must be registered in the container.**
dioxide:
1. Reads the type hints from your constructor (`db: Connection`)
2. Looks up `Connection` in the container registry
3. Resolves `Connection` (creating an instance if needed)
4. Passes it to your constructor
### The Dependency Must Be Registered
Constructor dependencies **must** be registered in the container before dioxide can inject them. There are three ways to register a dependency:
#### Option 1: Make it an Adapter for a Port
The most common pattern - create a port and register an adapter:
```python
from typing import Protocol
from dioxide import Container, Profile, adapter, service
# Define a port (interface) for database connections
class DatabaseConnection(Protocol):
def execute(self, sql: str) -> Any: ...
# Register an adapter for the port
@adapter.for_(DatabaseConnection, profile=Profile.PRODUCTION)
class SqliteConnection:
def __init__(self) -> None:
import sqlite3
self.conn = sqlite3.connect("app.db")
def execute(self, sql: str) -> Any:
return self.conn.execute(sql)
# Now other adapters can depend on DatabaseConnection
@adapter.for_(UserRepository, profile=Profile.PRODUCTION)
class SqliteUserRepository:
def __init__(self, db: DatabaseConnection) -> None: # Auto-injected!
self.db = db
async def get_user(self, user_id: str) -> dict | None:
result = self.db.execute(f"SELECT * FROM users WHERE id = ?", (user_id,))
# ...
```
#### Option 2: Make it a Service
If the dependency is core domain logic (not infrastructure), use `@service`:
```python
from dioxide import service
@service
class AppConfig:
"""Configuration loaded from environment."""
def __init__(self) -> None:
import os
self.database_url = os.getenv("DATABASE_URL", "sqlite:///dev.db")
self.sendgrid_api_key = os.getenv("SENDGRID_API_KEY", "")
# Adapters can depend on services
@adapter.for_(EmailPort, profile=Profile.PRODUCTION)
class SendGridAdapter:
def __init__(self, config: AppConfig) -> None: # Auto-injected!
self.api_key = config.sendgrid_api_key
```
#### Option 3: Manual Registration
For external dependencies or special cases, register manually:
```python
import sqlite3
from dioxide import Container, Profile
container = Container()
# Register a factory that creates the dependency
container.register_singleton(sqlite3.Connection, lambda: sqlite3.connect("app.db"))
# Now scan - adapters depending on sqlite3.Connection will get it injected
container.scan(profile=Profile.PRODUCTION)
```
### Complete Example: Adapters with Dependencies
Here's a complete example showing adapters that depend on other components:
```python
from typing import Protocol
from dioxide import Container, Profile, adapter, service
# --- Ports (interfaces) ---
class ConfigPort(Protocol):
"""Port for configuration access."""
def get(self, key: str, default: str = "") -> str:
"""Get configuration value by key."""
...
class EmailPort(Protocol):
async def send(self, to: str, subject: str, body: str) -> None: ...
class UserRepository(Protocol):
async def save(self, user: dict) -> dict: ...
async def get(self, user_id: str) -> dict | None: ...
# --- Configuration adapter ---
@adapter.for_(ConfigPort, profile=Profile.PRODUCTION)
class EnvConfigAdapter:
"""Configuration from environment variables."""
def get(self, key: str, default: str = "") -> str:
import os
return os.environ.get(key, default)
@adapter.for_(ConfigPort, profile=Profile.TEST)
class FakeConfigAdapter:
"""Fake configuration for testing."""
def __init__(self) -> None:
self.values = {"SENDGRID_API_KEY": "test-key", "DATABASE_URL": ":memory:"}
def get(self, key: str, default: str = "") -> str:
return self.values.get(key, default)
# --- Email adapter (depends on ConfigPort) ---
@adapter.for_(EmailPort, profile=Profile.PRODUCTION)
class SendGridAdapter:
def __init__(self, config: ConfigPort) -> None: # ConfigPort auto-injected!
self.api_key = config.get("SENDGRID_API_KEY")
async def send(self, to: str, subject: str, body: str) -> None:
# Use self.api_key to call SendGrid API
print(f"Sending via SendGrid to {to}: {subject}")
@adapter.for_(EmailPort, profile=Profile.TEST)
class FakeEmailAdapter:
def __init__(self) -> None: # No dependencies needed for test fake
self.sent_emails: list[dict] = []
async def send(self, to: str, subject: str, body: str) -> None:
self.sent_emails.append({"to": to, "subject": subject, "body": body})
# --- Service (depends on ports) ---
@service
class UserService:
def __init__(self, repo: UserRepository, email: EmailPort) -> None:
self.repo = repo
self.email = email
async def register_user(self, name: str, email_addr: str) -> dict:
user = await self.repo.save({"name": name, "email": email_addr})
await self.email.send(email_addr, "Welcome!", f"Hello {name}!")
return user
# --- Usage ---
container = Container()
container.scan(profile=Profile.PRODUCTION)
# When UserService is resolved:
# 1. dioxide sees UserService needs UserRepository and EmailPort
# 2. Resolves UserRepository -> gets SqliteUserRepository (if registered)
# 3. Resolves EmailPort -> gets SendGridAdapter
# 4. SendGridAdapter needs ConfigPort -> gets EnvConfigAdapter
# 5. Everything is wired up automatically!
service = container.resolve(UserService)
```
### Key Points
1. **Type hints are required**: Constructor parameters must have type hints for injection to work
2. **Dependencies must be registered**: Either via `@adapter.for_()`, `@service`, or manual registration
3. **Resolution is recursive**: If your dependency has dependencies, those are resolved too
4. **Circular dependencies are detected**: dioxide fails fast if A depends on B and B depends on A
5. **Test fakes often have no dependencies**: Fakes are typically simpler and don't need injection
### Common Patterns
**Config Adapter Pattern**: Create a `ConfigPort` that adapters depend on for configuration:
```python
@adapter.for_(EmailPort, profile=Profile.PRODUCTION)
class SendGridAdapter:
def __init__(self, config: ConfigPort) -> None:
self.api_key = config.get("SENDGRID_API_KEY")
```
**Database Connection Pattern**: Wrap database connections in a port for injection:
```python
@adapter.for_(DatabasePort, profile=Profile.PRODUCTION)
class PostgresAdapter:
def __init__(self, config: ConfigPort) -> None:
self.url = config.get("DATABASE_URL")
```
**Test Fakes Without Dependencies**: Test adapters are often simple and don't need dependencies:
```python
@adapter.for_(EmailPort, profile=Profile.TEST)
class FakeEmailAdapter:
def __init__(self) -> None: # No dependencies - just in-memory storage
self.sent_emails = []
```
## Lifecycle Management
Services and adapters can opt into lifecycle management using the `@lifecycle` decorator for components that need initialization and cleanup:
```python
from dioxide import Container, Profile, service, lifecycle, adapter
from typing import Protocol
# Port for cache operations
class CachePort(Protocol):
async def get(self, key: str) -> str | None: ...
async def set(self, key: str, value: str) -> None: ...
# Production adapter with lifecycle
@adapter.for_(CachePort, profile=Profile.PRODUCTION)
@lifecycle
class RedisAdapter:
"""Redis cache with connection lifecycle."""
def __init__(self, config: AppConfig):
self.config = config
self.redis = None
async def initialize(self) -> None:
"""Called automatically when container starts."""
self.redis = await aioredis.create_redis_pool(self.config.redis_url)
print(f"Redis connected: {self.config.redis_url}")
async def dispose(self) -> None:
"""Called automatically when container stops."""
if self.redis:
self.redis.close()
await self.redis.wait_closed()
print("Redis connection closed")
async def get(self, key: str) -> str | None:
return await self.redis.get(key)
async def set(self, key: str, value: str) -> None:
await self.redis.set(key, value)
# Service with lifecycle
@service
@lifecycle
class Database:
"""Database service with connection lifecycle."""
def __init__(self, config: AppConfig):
self.config = config
self.engine = None
async def initialize(self) -> None:
"""Called automatically when container starts."""
self.engine = create_async_engine(self.config.database_url)
print(f"Database connected: {self.config.database_url}")
async def dispose(self) -> None:
"""Called automatically when container stops."""
if self.engine:
await self.engine.dispose()
print("Database connection closed")
# Use async context manager for automatic lifecycle
container = Container()
container.scan(profile=Profile.PRODUCTION)
async with container:
# All @lifecycle components initialized here (in dependency order)
app = container.resolve(Application)
await app.run()
# All @lifecycle components disposed here (in reverse order)
# Or manually control lifecycle
await container.start() # Initialize all @lifecycle components
try:
app = container.resolve(Application)
await app.run()
finally:
await container.stop() # Dispose all @lifecycle components
```
**Why `@lifecycle`?**
- β
**Optional**: Only components that need it use lifecycle (test fakes typically don't!)
- β
**Validated**: Decorator ensures `initialize()` and `dispose()` methods exist and are async
- β
**Consistent**: Matches dioxide's decorator-based API (`@service`, `@adapter.for_()`)
- β
**Type-safe**: Type stubs provide IDE autocomplete and mypy validation
- β
**Dependency-ordered**: Components initialized/disposed in correct dependency order
**Key Features**:
- **Async context manager**: `async with container:` handles start/stop automatically
- **Manual control**: `await container.start()` and `await container.stop()` for explicit lifecycle
- **Dependency ordering**: Initialization happens in dependency order (dependencies first)
- **Reverse disposal**: Disposal happens in reverse order (dependents disposed before dependencies)
- **Graceful rollback**: If initialization fails, already-initialized components are disposed
- **Error resilience**: Disposal continues even if individual components fail
**Status**: Fully implemented.
## Multi-Binding (Collection Injection)
For plugin systems where multiple implementations should be collected rather than selecting one, dioxide supports **multi-binding**:
```python
from typing import Protocol
from dioxide import Container, Profile, adapter, service
# Define a port for plugins
class PluginPort(Protocol):
def process(self, data: str) -> str: ...
# Register MULTIPLE adapters with multi=True
@adapter.for_(PluginPort, multi=True, priority=10)
class ValidationPlugin:
def process(self, data: str) -> str:
# Validate data
return data
@adapter.for_(PluginPort, multi=True, priority=20)
class TransformPlugin:
def process(self, data: str) -> str:
# Transform data
return data.upper()
@adapter.for_(PluginPort, multi=True, priority=30)
class LoggingPlugin:
def process(self, data: str) -> str:
print(f"Processing: {data}")
return data
# Inject ALL plugins as a list
@service
class DataProcessor:
def __init__(self, plugins: list[PluginPort]) -> None:
self.plugins = plugins # All plugins, ordered by priority
def run(self, data: str) -> str:
for plugin in self.plugins:
data = plugin.process(data)
return data
# Usage
container = Container()
container.scan(profile=Profile.PRODUCTION)
processor = container.resolve(DataProcessor)
# processor.plugins == [ValidationPlugin, TransformPlugin, LoggingPlugin]
# Ordered by priority (lowest first)
result = processor.run("hello")
# Processes through all plugins in priority order
```
**Key features:**
- **`multi=True`**: Enables multi-binding mode (default is `False` for single adapter)
- **`priority=N`**: Controls ordering (lower values first, default is 0)
- **`list[Port]`**: Type hint tells container to inject all multi-bindings
- **Profile filtering**: Only adapters matching the active profile are included
- **Empty list OK**: Returns empty list if no implementations (valid for optional plugins)
**Constraints:**
- A port must be either single-binding OR multi-binding, not both
- Error at startup if same port has both `multi=True` and `multi=False` adapters
**Use cases:**
- Plugin systems (mutation operators, validators, transformers)
- Pipeline stages that run in sequence
- Event handlers that all process the same event
- Middleware chains
## Function Injection
dioxide works with **any callable**, not just classes. You can inject dependencies into standalone functions, route handlers, and background tasks by using default parameters with `container.resolve()`:
### Standalone Functions
```python
from dioxide import Container, Profile, adapter, service
from typing import Protocol
# Define port
class EmailPort(Protocol):
async def send(self, to: str, subject: str, body: str) -> None: ...
# Set up container
container = Container()
container.scan(profile=Profile.PRODUCTION)
# Standalone function with injected dependencies
async def send_welcome_email(
user_email: str,
user_name: str,
email: EmailPort = container.resolve(EmailPort)
) -> None:
"""Send welcome email using injected email service."""
await email.send(
to=user_email,
subject="Welcome!",
body=f"Thanks for joining, {user_name}!"
)
# Call like a normal function
await send_welcome_email("alice@example.com", "Alice")
# EmailPort dependency injected automatically
```
### Route Handlers (Web Frameworks)
Perfect for FastAPI, Flask, or any web framework:
```python
from fastapi import FastAPI, Request
from dioxide import Container, Profile
app = FastAPI()
container = Container()
container.scan(profile=Profile.PRODUCTION)
@app.post("/users")
async def create_user(
request: Request,
db: DatabasePort = container.resolve(DatabasePort),
email: EmailPort = container.resolve(EmailPort)
) -> dict:
"""Create user with injected database and email services."""
# Parse request
user_data = await request.json()
# Use injected dependencies
user = await db.save_user(user_data)
await email.send(
to=user_data["email"],
subject="Welcome!",
body=f"Hello {user_data['name']}!"
)
return {"id": user.id, "status": "created"}
```
### Background Tasks
Great for Celery, RQ, or any background job system:
```python
from dioxide import Container, Profile
from typing import Protocol
# Define ports
class PaymentPort(Protocol):
async def charge(self, invoice_id: str) -> dict: ...
class InvoiceEmailPort(Protocol):
"""Port for sending invoice-related emails."""
async def send_receipt(self, email: str, invoice: dict) -> None: ...
class LoggerPort(Protocol):
"""Port for logging."""
def info(self, msg: str) -> None: ...
def error(self, msg: str) -> None: ...
# Set up container
container = Container()
container.scan(profile=Profile.PRODUCTION)
# Background task with injected dependencies
async def process_invoice(
invoice_id: str,
payment: PaymentPort = container.resolve(PaymentPort),
email: InvoiceEmailPort = container.resolve(InvoiceEmailPort),
logger: LoggerPort = container.resolve(LoggerPort)
) -> None:
"""Process invoice payment and send receipt."""
try:
# Charge payment
result = await payment.charge(invoice_id)
# Send receipt
await email.send_receipt(result["customer_email"], result)
# Log success
logger.info(f"Invoice {invoice_id} processed successfully")
except Exception as e:
logger.error(f"Failed to process invoice {invoice_id}: {e}")
raise
# Schedule the task (example with Celery)
@celery_app.task
def process_invoice_task(invoice_id: str):
"""Celery task wrapper."""
import asyncio
return asyncio.run(process_invoice(invoice_id))
```
### Testing Functions with Injection
Function injection works seamlessly with the profile system for testing:
```python
import pytest
from dioxide import Container, Profile
@pytest.fixture
def test_container():
"""Container with test profile."""
container = Container()
container.scan(profile=Profile.TEST)
return container
async def test_send_welcome_email(test_container):
"""Test function injection with fake email adapter."""
# Function uses test profile automatically
await send_welcome_email("test@example.com", "TestUser")
# Verify with fake adapter
fake_email = test_container.resolve(EmailPort)
assert len(fake_email.sent_emails) == 1
assert fake_email.sent_emails[0]["to"] == "test@example.com"
```
**Why function injection?**
- β
**Flexible**: Works with any callable (classes, functions, lambdas)
- β
**Practical**: Perfect for route handlers, background jobs, utility functions
- β
**Testable**: Same profile system works for function injection
- β
**No magic**: Just default parameters with `container.resolve()`
- β
**Type-safe**: Full mypy support for injected types
### A Note on This Pattern
> **Transparency**: The `container.resolve()` default-arg pattern is service-locator-adjacent. We intentionally support it for zero-ceremony use cases.
**When this pattern shines:**
- CLI commands with Click integration
- Simple scripts and small applications
- Prototyping and exploration
- Route handlers in small web apps
**When to prefer explicit injection:**
For larger codebases with many dependencies, prefer explicit constructor injectionβit's more testable and makes dependencies visible:
```python
# Service-locator-adjacent (convenient for small apps)
async def send_welcome_email(
user_email: str,
email: EmailPort = container.resolve(EmailPort)
) -> None:
await email.send(to=user_email, subject="Welcome!", body="...")
# Explicit injection (recommended for larger codebases)
@service
class WelcomeEmailHandler:
def __init__(self, email: EmailPort):
self.email = email
async def send(self, user_email: str) -> None:
await self.email.send(to=user_email, subject="Welcome!", body="...")
```
Both patterns work with dioxide's profile system for testing. Choose based on your codebase size and team preferences.
## Testing with dioxide
dioxide makes testing easy through **fakes at the seams** instead of mocks. The key pattern is creating a fresh `Container` instance per test for complete isolation.
> **[π Full Testing Guide](https://dioxide.readthedocs.io/en/latest/testing/)** - Comprehensive patterns for testing with dioxide
### Fresh Container Per Test (Recommended)
The simplest approach uses the `fresh_container` helper from `dioxide.testing`:
```python
import pytest
from dioxide import Profile
from dioxide.testing import fresh_container
@pytest.fixture
async def container():
"""Fresh container per test - complete test isolation."""
async with fresh_container(profile=Profile.TEST) as c:
yield c
# Cleanup happens automatically
```
Or create the container manually for more control:
```python
import pytest
from dioxide import Container, Profile
@pytest.fixture
async def container():
"""Fresh container per test - complete test isolation.
Each test gets a fresh Container instance with:
- Clean singleton cache (no state from previous tests)
- Fresh adapter instances
- Automatic lifecycle management via async context manager
This is the RECOMMENDED pattern for test isolation.
"""
c = Container()
c.scan(profile=Profile.TEST)
async with c:
yield c
# Cleanup happens automatically
```
**Why this works:**
- **Complete isolation**: Each test starts with a clean slate
- **No state leakage**: Singletons are scoped to the container instance
- **Lifecycle handled**: `@lifecycle` components are properly initialized/disposed
- **Simple**: No need to track or clear fake state
### Typed Fixtures for Fakes
Create typed fixtures to access your fake adapters with IDE support:
```python
from app.adapters.fakes import FakeEmailAdapter, FakeDatabaseAdapter
from app.domain.ports import EmailPort, DatabasePort
@pytest.fixture
def email(container) -> FakeEmailAdapter:
"""Typed access to fake email for assertions."""
return container.resolve(EmailPort)
@pytest.fixture
def db(container) -> FakeDatabaseAdapter:
"""Typed access to fake db for seeding test data."""
return container.resolve(DatabasePort)
```
### Complete Test Example
```python
async def test_user_registration_sends_welcome_email(container, email, db):
"""Test that registering a user sends a welcome email."""
# Arrange: Get the service (dependencies auto-injected)
service = container.resolve(UserService)
# Act: Call the real service with real fakes
await service.register_user("alice@example.com", "Alice")
# Assert: Check observable outcomes (no mock verification!)
assert len(email.sent_emails) == 1
assert email.sent_emails[0]["to"] == "alice@example.com"
assert "Welcome" in email.sent_emails[0]["subject"]
```
**Benefits over mocking:**
- **Test real behavior**: Business logic actually runs
- **No brittle mocks**: Tests don't break when you refactor
- **Fast**: In-memory fakes, no I/O
- **Deterministic**: Controllable fakes (FakeClock, etc.)
### Alternative: Reset Container Between Tests
If you need a shared container (e.g., for TestClient integration tests), use `container.reset()`:
```python
from dioxide import container, Profile
@pytest.fixture(autouse=True)
def setup_container():
"""Reset container between tests for isolation."""
container.scan(profile=Profile.TEST)
yield
container.reset() # Clears singleton cache, keeps registrations
```
Or clear fake state manually if you need more control:
```python
@pytest.fixture(autouse=True)
def clear_fakes():
"""Clear fake state before each test."""
# Clear adapters from global container before test runs
db = container.resolve(DatabasePort)
if hasattr(db, "users"):
db.users.clear()
email = container.resolve(EmailPort)
if hasattr(email, "sent_emails"):
email.sent_emails.clear()
```
**Note**: The fresh container pattern is preferred because it requires no knowledge of fake internals.
For comprehensive testing patterns, see [TESTING_GUIDE.md](docs/TESTING_GUIDE.md).
## Framework Integrations
dioxide provides seamless integration with popular Python web frameworks.
### FastAPI Integration
```python
from fastapi import FastAPI
from dioxide import Profile
from dioxide.fastapi import DioxideMiddleware, Inject
app = FastAPI()
app.add_middleware(DioxideMiddleware, profile=Profile.PRODUCTION, packages=["myapp"])
@app.get("/users")
async def list_users(service: UserService = Inject(UserService)):
return await service.list_all()
```
Install with: `pip install dioxide[fastapi]`
**Features**: Single middleware setup, automatic container lifecycle, REQUEST-scoped components, sync/async support.
See the complete [FastAPI example](examples/fastapi/) for a full hexagonal architecture application.
### Django Integration
```python
# settings.py or apps.py
from dioxide import Profile
from dioxide.django import configure_dioxide
configure_dioxide(profile=Profile.PRODUCTION, packages=["myapp"])
# settings.py - add middleware
MIDDLEWARE = [
...
'dioxide.django.DioxideMiddleware',
...
]
# views.py
from dioxide.django import inject
def user_list(request):
service = inject(UserService)
return JsonResponse({"users": service.list_all()})
```
Install with: `pip install dioxide[django]`
**Features**: Single function setup, request scoping via middleware, thread-safe injection, works with class-based and function-based views.
### Django REST Framework Integration
dioxide works seamlessly with Django REST Framework - just use the same `inject()` function in your APIViews, ViewSets, or `@api_view` decorated functions:
```python
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.views import APIView
from dioxide.django import inject
@api_view(['GET'])
def user_list(request):
service = inject(UserService)
return Response(service.list_all())
class UserViewSet(APIView):
def get(self, request):
service = inject(UserService)
return Response(service.list_all())
```
**Features**: Full DRF compatibility, works with all DRF view types (APIView, ViewSet, @api_view), request scoping through Django middleware.
See [Django Integration Guide](docs/integrations/django.md) for complete documentation including request scoping and lifecycle management.
## Features
### v1.0.0 STABLE - MLP Production Ready
**Core Dependency Injection**:
- [x] `@adapter.for_(Port, profile=...)` decorator for hexagonal architecture
- [x] `@service` decorator for core business logic
- [x] `Profile` enum (PRODUCTION, TEST, DEVELOPMENT, STAGING, CI, ALL)
- [x] Container with `scan(profile=...)` for profile-based activation
- [x] Port-based resolution (`container.resolve(Port)` returns active adapter)
- [x] Constructor dependency injection via type hints
- [x] Type-safe `Container.resolve()` with full mypy support
- [x] Optional `container[Type]` bracket syntax
- [x] Multi-binding support (`multi=True`, `priority=N`) for plugin patterns
- [x] Collection injection via `list[Port]` type hint
**Lifecycle Management**:
- [x] `@lifecycle` decorator for opt-in lifecycle management
- [x] `async with container:` context manager support
- [x] Manual `container.start()` / `container.stop()` methods
- [x] Dependency-ordered initialization (Kahn's algorithm)
- [x] Reverse-order disposal with error resilience
- [x] Graceful rollback on initialization failures
**Test Ergonomics**:
- [x] `fresh_container()` helper for isolated test containers
- [x] `container.reset()` to clear singleton cache between tests
- [x] `scope=Scope.FACTORY` on adapters for fresh instances per resolution
- [x] Complete test isolation with fresh Container per test pattern
**Reliability**:
- [x] Circular dependency detection at startup (fail-fast)
- [x] Excellent error messages with actionable suggestions
- [x] Package scanning: `container.scan(package="app.services")`
- [x] High test coverage (~93%, 213+ tests)
- [x] Full CI/CD automation with multi-platform wheels
**Documentation & Examples**:
- [x] **[ReadTheDocs](https://dioxide.readthedocs.io)** - Full documentation with API reference
- [x] Complete FastAPI integration example
- [x] Comprehensive testing guide (fakes > mocks philosophy)
- [x] Performance benchmarks with [honest comparisons](benchmarks/)
- [x] Tutorials, guides, and architectural patterns
- [x] Migration guides for all versions
**Production Ready**:
- [x] API frozen - No breaking changes until v2.0
- [x] Published to PyPI with cross-platform wheels
- [x] Battle-tested in real applications
- [x] Ready for production deployment
### Post-MLP Features (v1.1.0+)
Future enhancements under consideration:
- Request scoping for web frameworks
- Property injection
- Django integration
- Developer tooling (CLI, IDE plugins)
## Development
### Prerequisites
- Python 3.11+
- Rust 1.70+
- [uv](https://github.com/astral-sh/uv) for Python package management
- [maturin](https://github.com/PyO3/maturin) for building Rust extensions
### Setup
```bash
# Clone the repository
git clone https://github.com/mikelane/dioxide.git
cd dioxide
# Install dependencies with uv (uses PEP 735 dependency groups)
uv venv
source .venv/bin/activate # or `.venv\Scripts\activate` on Windows
uv sync --group dev
# Build the Rust extension
maturin develop
# Run tests
pytest
# Run all quality checks
tox
```
### Development Workflow
```bash
# Format code
tox -e format
# Lint
tox -e lint
# Type check
tox -e type
# Run tests for all Python versions
tox
# Run tests with coverage
tox -e cov
# Mutation testing
tox -e mutate
```
### Pre-commit Hooks
Install pre-commit hooks to ensure code quality:
```bash
pre-commit install
```
## Architecture
```
dioxide/
βββ python/dioxide/ # Python API
β βββ __init__.py
β βββ container.py # Main Container class
β βββ decorators.py # @component decorator
β βββ scope.py # Scope enum
βββ rust/src/ # Rust core
β βββ lib.rs # PyO3 bindings and graph logic
βββ tests/ # Python tests
βββ pyproject.toml # Project configuration
```
### Key Design Decisions
1. **Python-first API** - Developers work in pure Python; Rust is an implementation detail
2. **Type hints as the contract** - Leverage Python's type system for DI metadata
3. **Hexagonal architecture by design** - `@adapter.for_(Port)` makes clean architecture obvious
4. **Profile-based testing** - Built-in support for swapping implementations by environment
5. **Rust for container operations** - Memory-efficient singleton caching and graph traversal
6. **Test-driven development** - Every feature starts with failing tests
## Comparison to Other Frameworks
| Feature | dioxide | dependency-injector | injector |
|---------|----------|---------------------|----------|
| Zero-ceremony API | β
| β | β
|
| Built-in Profile system | β
| β | β |
| Hexagonal architecture support | β
| β | β |
| Type-based DI | β
| β
| β
|
| Lifecycle management | β
| β
| β |
| Circular dependency detection | β
| β | β |
| Memory efficiency | β
| β‘ | β‘ |
**Performance notes**: Both dioxide (Rust/PyO3) and dependency-injector (Cython) offer excellent performance for cached singleton resolution. dependency-injector's mature Cython backend is slightly faster for raw lookups; dioxide is more memory-efficient. Both handle concurrent workloads equally well. See [benchmarks/](benchmarks/) for detailed, honest comparisons.
## Contributing
Contributions are welcome! We follow a strict workflow to maintain code quality:
**Quick start for contributors:**
1. **Create or find an issue** - All work must be associated with a GitHub issue
2. **Fork the repository** (external contributors)
3. **Create a feature branch** with issue reference (e.g., `fix/issue-123-description`)
4. **Follow TDD** - Write tests first, then implementation
5. **Submit a Pull Request** - All changes must go through PR review
**Key requirements:**
- β
All work must have an associated GitHub issue
- β
All changes must go through the Pull Request process
- β
Tests and documentation are mandatory
- β
Branch protection enforces these requirements on `main`
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
**Resources:**
- **[π Documentation](https://dioxide.readthedocs.io)** - Full documentation with tutorials and API reference
- **[πΊοΈ Roadmap](ROADMAP.md)** - Complete product roadmap
- **[π‘ Design Philosophy](docs/MLP_VISION.md)** - MLP vision and architectural decisions
- **[π§ͺ Testing Guide](docs/TESTING_GUIDE.md)** - Comprehensive testing patterns (fakes > mocks)
## License
MIT License - see [LICENSE](LICENSE) for details.
## Acknowledgments
- Inspired by [dependency-injector](https://github.com/ets-labs/python-dependency-injector) and Spring Framework
- Built with [PyO3](https://github.com/PyO3/pyo3) and [maturin](https://github.com/PyO3/maturin)
- Graph algorithms powered by [petgraph](https://github.com/petgraph/petgraph)
---
**Production Ready**: dioxide v1.0.0 is production-ready with a stable, frozen API. All MLP (Minimum Loveable Product) features are complete, thoroughly tested, and battle-proven.
**API Guarantee**: No breaking changes until v2.0. Your code written against v1.x will continue to work through all v1.x releases.
**What's Next**:
- **v1.1.0+**: Post-MLP enhancements (request scoping, property injection, additional framework integrations)
- **v2.0.0+**: Major enhancements based on community needs
**Get Started**: **[Read the Documentation](https://dioxide.readthedocs.io)** | **[Install from PyPI](https://pypi.org/project/dioxide/)** | **[View on GitHub](https://github.com/mikelane/dioxide)**