DEV Community

Mike Lane
Mike Lane

Posted on

Dioxide v2.0.0: Clean Architecture Made Simple

TL;DR: Dioxide is a dependency injection framework for Python that makes hexagonal architecture the path of least resistance. v2.0.0 adds constructor-based profile scanning, extensible custom profiles, and introspection APIs.

What is Dioxide?

Dioxide's decorators encode hexagonal architecture patterns directly into your code. @adapter.for_(Port, profile=...) forces you to declare which abstraction you're implementing and when it applies. @service marks core domain logic that depends on ports, not concrete implementations. The result: clean architecture becomes the easiest path forward.

from dioxide import Container, adapter, service, Profile
from typing import Protocol

# Define your port (interface)
class EmailPort(Protocol):
    async def send(self, to: str, body: str) -> None: ...

# Production adapter - real infrastructure
@adapter.for_(EmailPort, profile=Profile.PRODUCTION)
class SendGridAdapter:
    async def send(self, to: str, body: str) -> None:
        # Real SendGrid API call
        ...

# Test adapter - fast fake
@adapter.for_(EmailPort, profile=Profile.TEST)
class FakeEmailAdapter:
    sent: list[dict] = []
    async def send(self, to: str, body: str) -> None:
        self.sent.append({"to": to, "body": body})

# Service depends on the PORT, not the concrete adapter
@service
class NotificationService:
    def __init__(self, email: EmailPort):
        self.email = email

# Production: uses SendGrid
async with Container(profile=Profile.PRODUCTION) as container:
    svc = container.resolve(NotificationService)

# Tests: uses fast fake, no network calls
async with Container(profile=Profile.TEST) as container:
    svc = container.resolve(NotificationService)
Enter fullscreen mode Exit fullscreen mode

What's New in v2.0.0

Auto-Scan via Constructor

Pass profile directly to the Container constructor. The container scans for registered components automatically:

# v2.0: profile in constructor triggers auto-scan
container = Container(profile=Profile.PRODUCTION)

# v1.x: explicit scan still works
container = Container()
container.scan(profile=Profile.PRODUCTION)
Enter fullscreen mode Exit fullscreen mode

Extensible Custom Profiles

Profile changed from StrEnum to an extensible str subclass. Create domain-specific profiles that work identically to built-ins:

LOAD_TEST = Profile("load-test")
INTEGRATION = Profile("integration")

@adapter.for_(DatabasePort, profile=INTEGRATION)
class IntegrationDatabaseAdapter:
    ...
Enter fullscreen mode Exit fullscreen mode

Container Introspection

Debug container state with list_registered():

container = Container(profile=Profile.TEST)
for registered_type in container.list_registered():
    print(f"Registered: {registered_type.__name__}")
Enter fullscreen mode Exit fullscreen mode

Pytest Fixtures

The dioxide.testing module provides isolation helpers:

from dioxide.testing import fresh_container

async def test_my_service():
    async with fresh_container(profile=Profile.TEST) as container:
        service = container.resolve(MyService)
        # Container is fresh, isolated, and auto-cleaned
Enter fullscreen mode Exit fullscreen mode

Breaking Changes

If you used Profile's StrEnum methods, update your code:

Old (v1.x) New (v2.0)
profile.value str(profile)
profile.name Use constant directly
for p in Profile: Explicit list

Why Dioxide?

  • Type-safe: Full mypy support catches type mismatches in constructor injection at lint time
  • Fast: Rust-backed container with sub-microsecond cached singleton lookup
  • Framework integrations: FastAPI, Flask, Django, Celery, Click
  • Testing-oriented: Profile system enables fast in-memory fakes instead of mocks

Get Started

pip install dioxide
Enter fullscreen mode Exit fullscreen mode

Documentation | GitHub

Top comments (0)