Why dioxide Exists¶
A philosophical essay on dependency injection, testing pain, and the path of least resistance
The Observation¶
Every experienced Python developer eventually notices the same pattern: the hardest code to test is always the code that talks to the outside world.
Database queries, API calls, file operations, sending emails, reading from message queues. These boundaries between your application and external systems are where complexity concentrates. They are also where most bugs hide, where tests become slow and flaky, and where refactoring feels dangerous.
This observation is not new. It has a name: tight coupling.
When your business logic is entangled with infrastructure concerns, you inherit all the problems of that infrastructure in your tests. Need to verify that your user registration logic sends a welcome email? You either mock sendgrid.send() and hope your mock behaves like the real thing, or you spin up a test email server and watch your test suite slow to a crawl.
Neither option feels right. The first tests mock behavior, not real behavior. The second defeats the purpose of unit testing.
The Insight¶
The solution to this problem has been known for decades. The SOLID principles, articulated by Robert Martin, include the Dependency Inversion Principle: depend on abstractions, not concretions.
In practice, this means your business logic should not import sendgrid or psycopg2 directly. Instead, it should depend on an abstraction, a port, that defines what operations it needs without specifying how they are implemented. Concrete implementations, adapters, fulfill these contracts.
This is hexagonal architecture, also known as ports-and-adapters. It is not a new idea. Alistair Cockburn described it in 2005. The concept dates back even further under different names.
Yet most Python codebases ignore it.
Why? Because in Python, tight coupling is easier. It is trivially easy to write from sendgrid import send_email at the top of your module and call it from your business logic. The path of least resistance leads directly to unmaintainable code.
dioxide exists because we believe this is a tooling problem, not a discipline problem.
If loose coupling were as easy as tight coupling, developers would naturally choose it. The framework’s job is to make the Dependency Inversion Principle feel inevitable.
The Design Decisions¶
Every design choice in dioxide flows from this central insight. Here is the reasoning behind the major decisions.
Decorators, Not Configuration Files¶
In Java, dependency injection often involves XML configuration files or annotation processors. This creates a separation between where you define your components and where you configure them.
dioxide uses Python decorators because Python is the configuration language.
@adapter.for_(EmailPort, profile=Profile.PRODUCTION)
class SendGridAdapter:
async def send(self, to: str, subject: str, body: str) -> None:
...
The decorator lives directly on the class. There is no separate XML file to maintain, no configuration to keep in sync, no indirection to trace. When you look at a class, you immediately know what port it implements and in which profile it is active.
This is explicit over clever. Boring over magical. It optimizes for reading code, because code is read far more often than it is written.
Profiles, Not Overrides¶
Many DI frameworks treat test doubles as second-class citizens. You register your “real” implementations, then use override mechanisms or mocking frameworks to substitute them in tests.
dioxide inverts this relationship. Test fakes are first-class adapters.
@adapter.for_(EmailPort, profile=Profile.PRODUCTION)
class SendGridAdapter:
...
@adapter.for_(EmailPort, profile=Profile.TEST)
class FakeEmailAdapter:
def __init__(self):
self.sent_emails = []
...
Both adapters are equally valid implementations of EmailPort. Neither is “real” and neither is a “mock”. They are simply different adapters for different contexts.
This design choice has a subtle but profound implication: your fakes live in production code, not test code. This means they are maintained alongside real implementations, they can be reused across your entire test suite, and they document the contract your ports require.
Protocols, Not Base Classes¶
Python’s typing.Protocol enables structural typing. A class implements a Protocol if it has the right methods with the right signatures, regardless of inheritance.
dioxide uses Protocols for ports because they align with how Python developers think:
class EmailPort(Protocol):
async def send(self, to: str, subject: str, body: str) -> None: ...
There is no abstract base class to inherit from. Your adapters do not need to know about each other. They simply implement the methods the port declares.
This is duck typing made type-safe. If it sends emails like an email sender, it is an email sender.
Type Hints as the Contract¶
The type checker is the source of truth for wiring. If your code passes mypy or pyright, your dependencies are wired correctly.
@service
class UserService:
def __init__(self, email: EmailPort):
self.email = email
dioxide reads the email: EmailPort type hint and knows to inject whatever adapter is registered for EmailPort in the active profile. No magic strings, no runtime reflection beyond what Python’s type system already provides.
This means IDE autocomplete guides you toward correct usage. Typos in port names become type errors, caught before you run a single test.
Rust Core, Python API¶
dioxide uses Rust for its container implementation. This is not for performance theater. It is for consistent performance under load.
Dependency resolution in dioxide is O(1) for cached singletons. Container initialization validates the entire dependency graph at startup, catching circular dependencies and missing adapters before your first request.
More importantly, the Rust implementation is a private implementation detail. Users interact entirely through Python. They never need to know Rust exists.
This is the inverse of the typical Python/Rust hybrid approach. Instead of exposing Rust performance to Python users, dioxide uses Rust to remove performance as a consideration entirely. Sub-microsecond resolution means DI overhead is negligible compared to any real work your application does.
The North Star¶
dioxide’s mission statement is simple:
Make the Dependency Inversion Principle feel inevitable.
This is not about forcing developers to write clean code. It is about removing the friction that makes clean code harder than dirty code.
When adding a new external integration, the easiest path should be:
Define a Protocol describing what you need
Create an adapter that implements it
Decorate with
@adapter.for_(Port, profile=...)
That is it. The container handles wiring. The profile system handles environment switching. Type hints ensure correctness.
The alternative, importing infrastructure directly into business logic, should feel like swimming upstream.
What We Learned¶
Building dioxide clarified several insights about software architecture:
Testing pain is architectural pain in disguise. When tests are hard to write, slow to run, or brittle under refactoring, the problem is rarely the testing framework. It is the architecture. Proper boundaries make testing trivial.
Mocking is a symptom, not a solution. Reaching for unittest.mock or pytest-mock is often a sign that you are testing the wrong thing. If you need to patch internal calls, your code has hidden dependencies. If you need to configure complex mock behavior, you are testing mock configuration, not your business logic.
Fakes are better than mocks. A simple in-memory implementation that captures calls and stores data is easier to understand, easier to maintain, and tests real behavior. It is just another adapter, living in your production code, ready for use in tests, development, and demos.
The seams matter. Ports define where your application ends and the world begins. Getting these boundaries right is most of architecture. Everything else follows.
The Future¶
dioxide is intentionally focused. It solves one problem well: making hexagonal architecture the path of least resistance in Python.
There are many features we explicitly choose not to build:
Configuration management (use Pydantic Settings)
Property or method injection (constructor injection only)
Circular dependency resolution (fix your architecture instead)
XML or YAML configuration (Python is the config language)
This restraint is not laziness. It is acknowledgment that trying to solve every problem makes you good at none of them.
What we will continue to develop:
Better developer experience: error messages that guide you toward solutions
Framework integrations: making dioxide work seamlessly with FastAPI, Django, Celery, and others
Performance: maintaining sub-microsecond resolution as features grow
Documentation: ensuring every capability is clearly explained
Conclusion¶
dioxide exists because we believe Python developers deserve better tools for building maintainable applications.
Not tools that force discipline through complexity. Tools that make the right thing easy.
Not frameworks that take over your application. Libraries that stay out of your way.
Not magic that obscures what is happening. Explicit, boring, obvious code that anyone can read.
The Dependency Inversion Principle is not new wisdom. Hexagonal architecture is not a novel pattern. What dioxide offers is a path of least resistance that leads to these destinations naturally.
Write your ports. Decorate your adapters. Let the container handle the wiring.
And finally, test your business logic with fast fakes instead of slow mocks.
That is all there is to it. That is why dioxide exists.
See Also¶
Getting Started - Hands-on tutorial to start using dioxide
Why dioxide? - Feature comparison with other DI frameworks
Dioxide Design Principles - Complete design specification and roadmap
Testing with dioxide - Testing philosophy: fakes over mocks
Hexagonal Architecture with dioxide - Ports and adapters pattern explained