Zero-ceremony dependency injection for Python with built-in hexagonal architecture
📖 Read the Documentation | 🚀 Quick Start | 💡 Examples | 📋 API Reference
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@serviceguide 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
pip install dioxide| Platform | x86_64 | ARM64/aarch64 |
|---|---|---|
| Linux | ✅ | ✅ |
| macOS | ✅ | ✅ (M1/M2/M3) |
| Windows | ✅ | ❌ |
Python Versions: 3.11, 3.12, 3.13, 3.14
✨ STABLE: dioxide v0.1.1 is production-ready with a stable, frozen API.
- Latest Release: v0.1.1 (Nov 25, 2025) - 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 for the complete migration guide.
See ROADMAP.md for post-MLP features and Issues for current work.
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 for the complete design philosophy and TESTING_GUIDE.md for testing patterns.
dioxide embraces hexagonal architecture (ports-and-adapters) to make clean, testable code the path of least resistance.
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("[email protected]", "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("[email protected]", "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"] == "[email protected]"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
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".
When you write:
@adapter.for_(UserRepository, profile=Profile.PRODUCTION)
class SqliteUserRepository:
def __init__(self, db: Connection) -> None: # <-- type hint tells dioxide what to inject
self.db = dbYou might ask: "How does dioxide know where to get db?"
The answer: The dependency must be registered in the container.
dioxide:
- Reads the type hints from your constructor (
db: Connection) - Looks up
Connectionin the container registry - Resolves
Connection(creating an instance if needed) - Passes it to your constructor
Constructor dependencies must be registered in the container before dioxide can inject them. There are three ways to register a dependency:
The most common pattern - create a port and register an adapter:
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,))
# ...If the dependency is core domain logic (not infrastructure), use @service:
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_keyFor external dependencies or special cases, register manually:
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)Here's a complete example showing adapters that depend on other components:
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)- Type hints are required: Constructor parameters must have type hints for injection to work
- Dependencies must be registered: Either via
@adapter.for_(),@service, or manual registration - Resolution is recursive: If your dependency has dependencies, those are resolved too
- Circular dependencies are detected: dioxide fails fast if A depends on B and B depends on A
- Test fakes often have no dependencies: Fakes are typically simpler and don't need injection
Config Adapter Pattern: Create a ConfigPort that adapters depend on for configuration:
@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:
@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:
@adapter.for_(EmailPort, profile=Profile.TEST)
class FakeEmailAdapter:
def __init__(self) -> None: # No dependencies - just in-memory storage
self.sent_emails = []Services and adapters can opt into lifecycle management using the @lifecycle decorator for components that need initialization and cleanup:
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 componentsWhy @lifecycle?
- ✅ Optional: Only components that need it use lifecycle (test fakes typically don't!)
- ✅ Validated: Decorator ensures
initialize()anddispose()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()andawait 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 (v0.0.4-alpha).
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():
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("[email protected]", "Alice")
# EmailPort dependency injected automaticallyPerfect for FastAPI, Flask, or any web framework:
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"}Great for Celery, RQ, or any background job system:
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))Function injection works seamlessly with the profile system for testing:
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("[email protected]", "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"] == "[email protected]"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
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 - Comprehensive patterns for testing with dioxide
The simplest approach uses the fresh_container helper from dioxide.testing:
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 automaticallyOr create the container manually for more control:
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 automaticallyWhy this works:
- Complete isolation: Each test starts with a clean slate
- No state leakage: Singletons are scoped to the container instance
- Lifecycle handled:
@lifecyclecomponents are properly initialized/disposed - Simple: No need to track or clear fake state
Create typed fixtures to access your fake adapters with IDE support:
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)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("[email protected]", "Alice")
# Assert: Check observable outcomes (no mock verification!)
assert len(email.sent_emails) == 1
assert email.sent_emails[0]["to"] == "[email protected]"
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.)
If you need a shared container (e.g., for TestClient integration tests), use container.reset():
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 registrationsOr clear fake state manually if you need more control:
@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.
dioxide provides seamless FastAPI integration via the optional dioxide.fastapi module:
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 - No ceremony, just add the middleware
- Automatic container lifecycle - Integrates with FastAPI lifespan events
- REQUEST-scoped components - Fresh instances per HTTP request
- Works with sync and async - Route handlers work seamlessly
See the complete FastAPI example for a full hexagonal architecture application.
Core Dependency Injection:
-
@adapter.for_(Port, profile=...)decorator for hexagonal architecture -
@servicedecorator for core business logic -
Profileenum (PRODUCTION, TEST, DEVELOPMENT, STAGING, CI, ALL) - Container with
scan(profile=...)for profile-based activation - Port-based resolution (
container.resolve(Port)returns active adapter) - Constructor dependency injection via type hints
- Type-safe
Container.resolve()with full mypy support - Optional
container[Type]bracket syntax
Lifecycle Management:
-
@lifecycledecorator for opt-in lifecycle management -
async with container:context manager support - Manual
container.start()/container.stop()methods - Dependency-ordered initialization (Kahn's algorithm)
- Reverse-order disposal with error resilience
- Graceful rollback on initialization failures
Test Ergonomics:
-
fresh_container()helper for isolated test containers -
container.reset()to clear singleton cache between tests -
scope=Scope.FACTORYon adapters for fresh instances per resolution - Complete test isolation with fresh Container per test pattern
Reliability:
- Circular dependency detection at startup (fail-fast)
- Excellent error messages with actionable suggestions
- Package scanning:
container.scan(package="app.services") - High test coverage (~93%, 213+ tests)
- Full CI/CD automation with multi-platform wheels
Documentation & Examples:
- ReadTheDocs - Full documentation with API reference
- Complete FastAPI integration example
- Comprehensive testing guide (fakes > mocks philosophy)
- Performance benchmarks with honest comparisons
- Tutorials, guides, and architectural patterns
- Migration guides for all versions
Production Ready:
- API frozen - No breaking changes until v2.0
- Published to PyPI with cross-platform wheels
- Battle-tested in real applications
- Ready for production deployment
See ROADMAP.md for post-MLP features:
- Request scoping
- Property injection
- Framework integrations (FastAPI, Flask, Django)
- Developer tooling (CLI, IDE plugins)
# 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# 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 mutateInstall pre-commit hooks to ensure code quality:
pre-commit installdioxide/
├── 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
- Python-first API - Developers work in pure Python; Rust is an implementation detail
- Type hints as the contract - Leverage Python's type system for DI metadata
- Hexagonal architecture by design -
@adapter.for_(Port)makes clean architecture obvious - Profile-based testing - Built-in support for swapping implementations by environment
- Rust for container operations - Memory-efficient singleton caching and graph traversal
- Test-driven development - Every feature starts with failing tests
| 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/ for detailed, honest comparisons.
Contributions are welcome! We follow a strict workflow to maintain code quality:
Quick start for contributors:
- Create or find an issue - All work must be associated with a GitHub issue
- Fork the repository (external contributors)
- Create a feature branch with issue reference (e.g.,
fix/issue-123-description) - Follow TDD - Write tests first, then implementation
- 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 for detailed guidelines.
Resources:
- 📖 Documentation - Full documentation with tutorials and API reference
- 🗺️ Roadmap - Complete product roadmap
- 💡 Design Philosophy - MLP vision and architectural decisions
- 🧪 Testing Guide - Comprehensive testing patterns (fakes > mocks)
MIT License - see LICENSE for details.
- Inspired by dependency-injector and Spring Framework
- Built with PyO3 and maturin
- Graph algorithms powered by petgraph
✨ Production Ready: dioxide v0.1.1 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 v0.1.x will continue to work through all v0.x and v1.x releases.
What's Next:
- v0.2.0+: Post-MLP enhancements (request scoping, property injection, framework integrations)
- v1.0.0: Stable release after ecosystem adoption and feedback
- v2.0.0+: Major enhancements based on community needs
Get Started: 📖 Read the Documentation | 📦 Install from PyPI | 💻 View on GitHub