https://github.com/maksimzayats/diwire
🔌 Type-safe dependency injection for Python with auto-wiring, scopes, async factories, and zero deps
https://github.com/maksimzayats/diwire
asyncio dependency-injection di ioc python type-hints
Last synced: 3 months ago
JSON representation
🔌 Type-safe dependency injection for Python with auto-wiring, scopes, async factories, and zero deps
- Host: GitHub
- URL: https://github.com/maksimzayats/diwire
- Owner: maksimzayats
- License: mit
- Created: 2026-01-25T14:11:23.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-01-30T15:34:55.000Z (4 months ago)
- Last Synced: 2026-01-31T00:43:17.034Z (4 months ago)
- Topics: asyncio, dependency-injection, di, ioc, python, type-hints
- Language: Python
- Homepage: https://pypi.org/project/diwire/
- Size: 425 KB
- Stars: 18
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
- awesome-dependency-injection-in-python - diwire - Type-safe dependency injection for Python with auto-wiring, scopes, async factories, and zero deps [🐍, MIT License]. (Software / DI Frameworks / Containers)
README
# diwire
**Type-driven dependency injection for Python. Zero dependencies. Zero boilerplate.**
[](https://pypi.org/project/diwire/)
[](https://pypi.org/project/diwire/)
[](LICENSE)
[](https://codecov.io/gh/MaksimZayats/diwire)
[](https://docs.diwire.dev)
diwire is a dependency injection container for Python 3.10+ that builds your object graph from type hints. It supports
scopes + deterministic cleanup, async resolution, open generics, fast steady-state resolution via compiled
resolvers, and free-threaded Python (no-GIL) — all with zero runtime dependencies.
## Why diwire
- **Zero runtime dependencies**: easy to adopt anywhere. ([Why diwire](https://docs.diwire.dev/why-diwire.html))
- **Scopes + deterministic cleanup**: generator/async-generator providers clean up on scope exit. ([Scopes](https://docs.diwire.dev/core/scopes.html))
- **Async resolution**: ``aresolve()`` mirrors ``resolve()`` and async providers are first-class. ([Async](https://docs.diwire.dev/core/async.html))
- **Open generics**: register once, resolve for many type parameters. ([Open generics](https://docs.diwire.dev/core/open-generics.html))
- **Function injection**: ``Injected[T]`` and ``FromContext[T]`` for ergonomic handlers. ([Function injection](https://docs.diwire.dev/core/function-injection.html))
- **Named components + collect-all**: ``Component("name")`` and ``All[T]``. ([Components](https://docs.diwire.dev/core/components.html))
- **Concurrency + free-threaded builds**: configurable locking via ``LockMode``. ([Concurrency](https://docs.diwire.dev/howto/advanced/concurrency.html))
## Performance (benchmarked)
Benchmarks + methodology live in the docs: [Performance](https://docs.diwire.dev/howto/advanced/performance.html).
In this benchmark suite on CPython ``3.14.3`` (Apple M3 Pro, strict mode):
- Speedup over ``rodi`` ranges from **1.54×** to **6.04×**.
- Speedup over ``dishka`` ranges from **2.94×** to **30.14×**.
- Speedup over ``wireup`` ranges from **1.84×** to **4.98×**.
- Resolve-only comparisons (includes ``punq`` in non-scope scenarios): speedup over ``punq`` ranges from **5.27×** to **595.70×**.
- Current benchmark totals: **10** full-suite scenarios and **4** resolve-only scenarios.
Results vary by environment, Python version, and hardware. Re-run ``make benchmark-report`` and
``make benchmark-report-resolve`` on your target runtime before drawing final conclusions for production workloads.
## Installation
```bash
uv add diwire
```
```bash
pip install diwire
```
## Quick start (auto-wiring)
Define your classes. Resolve the top-level one. diwire figures out the rest.
```python
from dataclasses import dataclass, field
from diwire import Container
@dataclass
class Database:
host: str = field(default="localhost", init=False)
@dataclass
class UserRepository:
db: Database
@dataclass
class UserService:
repo: UserRepository
container = Container()
service = container.resolve(UserService)
print(service.repo.db.host) # => localhost
```
## Registration
Use explicit registrations when you need configuration objects, interfaces/protocols, cleanup, or multiple
implementations.
**Strict mode (opt-in):**
```python
from diwire import Container, DependencyRegistrationPolicy, MissingPolicy
container = Container(
missing_policy=MissingPolicy.ERROR,
dependency_registration_policy=DependencyRegistrationPolicy.IGNORE,
)
```
``Container()`` enables recursive auto-wiring by default. Use strict mode when you need full
control over registration and want missing dependencies to fail fast.
```python
from typing import Protocol
from diwire import Container, Lifetime
class Clock(Protocol):
def now(self) -> str: ...
class SystemClock:
def now(self) -> str:
return "now"
container = Container()
container.add(
SystemClock,
provides=Clock,
lifetime=Lifetime.SCOPED,
)
print(container.resolve(Clock).now()) # => now
```
Register factories directly:
```python
from diwire import Container
container = Container()
def build_answer() -> int:
return 42
container.add_factory(build_answer)
print(container.resolve(int)) # => 42
```
## Scopes & cleanup
Use `Lifetime.SCOPED` for per-request/per-job caching. Use generator/async-generator providers for deterministic
cleanup on scope exit.
```python
from collections.abc import Generator
from diwire import Container, Lifetime, Scope
class Session:
def __init__(self) -> None:
self.closed = False
def close(self) -> None:
self.closed = True
def session_factory() -> Generator[Session, None, None]:
session = Session()
try:
yield session
finally:
session.close()
container = Container()
container.add_generator(
session_factory,
provides=Session,
scope=Scope.REQUEST,
lifetime=Lifetime.SCOPED,
)
with container.enter_scope() as request_scope:
session = request_scope.resolve(Session)
print(session.closed) # => False
print(session.closed) # => True
```
## Function injection
Mark injected parameters as `Injected[T]` and wrap callables with `@resolver_context.inject`.
```python
from diwire import Container, Injected, resolver_context
class Service:
def run(self) -> str:
return "ok"
container = Container()
container.add(Service)
@resolver_context.inject
def handler(service: Injected[Service]) -> str:
return service.run()
print(handler()) # => ok
```
## Named components
Use `Annotated[T, Component("name")]` when you need multiple registrations for the same base type.
For registration ergonomics, you can also pass `component="name"` to `add_*` methods.
```python
from typing import Annotated, TypeAlias
from diwire import All, Component, Container
class Cache:
def __init__(self, label: str) -> None:
self.label = label
PrimaryCache: TypeAlias = Annotated[Cache, Component("primary")]
FallbackCache: TypeAlias = Annotated[Cache, Component("fallback")]
container = Container()
container.add_instance(Cache(label="redis"), provides=Cache, component="primary")
container.add_instance(Cache(label="memory"), provides=Cache, component="fallback")
print(container.resolve(PrimaryCache).label) # => redis
print(container.resolve(FallbackCache).label) # => memory
print([cache.label for cache in container.resolve(All[Cache])]) # => ['redis', 'memory']
```
Resolution/injection keys are still `Annotated[..., Component(...)]` at runtime.
## resolver_context (optional)
If you can't (or don't want to) pass a resolver everywhere, use `resolver_context`.
It is a `contextvars`-based helper used by `@resolver_context.inject` and (by default) by `Container` resolution methods.
Inside `with container.enter_scope(...):`, injected callables resolve from the bound scope resolver; otherwise they fall
back to the container registered as the `resolver_context` fallback (`Container(..., use_resolver_context=True)` is the
default).
```python
from diwire import Container, FromContext, Scope, resolver_context
container = Container()
@resolver_context.inject(scope=Scope.REQUEST)
def handler(value: FromContext[int]) -> int:
return value
with container.enter_scope(Scope.REQUEST, context={int: 7}):
print(handler()) # => 7
```
## Stability
diwire targets a stable, small public API.
- Backward-incompatible changes only happen in major releases.
- Deprecations are announced first and kept for at least one minor release (when practical).
## Docs
- [Tutorial (runnable examples)](https://docs.diwire.dev/howto/examples/)
- [Examples (repo)](https://github.com/maksimzayats/diwire/blob/main/examples/README.md)
- [Core concepts](https://docs.diwire.dev/core/)
- [API reference](https://docs.diwire.dev/reference/)
## License
MIT. See [LICENSE](LICENSE).