Skip to main content

A lightweight, type-safe dependency injection container with automatic wiring, scoped lifetimes, and zero dependencies

Project description

diwire

Type-driven dependency injection for Python. Zero dependencies. Zero boilerplate.

PyPI version Python versions License: MIT codecov Docs

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)
  • Scopes + deterministic cleanup: generator/async-generator providers clean up on scope exit. (Scopes)
  • Async resolution: aresolve() mirrors resolve() and async providers are first-class. (Async)
  • Open generics: register once, resolve for many type parameters. (Open generics)
  • Function injection: Injected[T] and FromContext[T] for ergonomic handlers. (Function injection)
  • Named components + collect-all: Component("name") and All[T]. (Components)
  • Concurrency + free-threaded builds: configurable locking via LockMode. (Concurrency)

Performance (benchmarked)

Benchmarks + methodology live in the docs: Performance.

In this benchmark suite on CPython 3.14.2 (Apple M1 Pro, strict mode):

  • Speedup over rodi ranges from 1.28× to 2.48×.
  • Speedup over dishka ranges from 2.03× to 7.62×.
  • Speedup over wireup ranges from 1.06× to 3.31×.
  • Resolve-only comparisons (includes punq in non-scope scenarios): speedup over punq ranges from 2.47× to 388.44×.
  • 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

uv add diwire
pip install diwire

Quick start (auto-wiring)

Define your classes. Resolve the top-level one. diwire figures out the rest.

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):

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.

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:

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.

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.

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.

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).

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

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

diwire-1.0.2.tar.gz (369.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

diwire-1.0.2-py3-none-any.whl (86.0 kB view details)

Uploaded Python 3

File details

Details for the file diwire-1.0.2.tar.gz.

File metadata

  • Download URL: diwire-1.0.2.tar.gz
  • Upload date:
  • Size: 369.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for diwire-1.0.2.tar.gz
Algorithm Hash digest
SHA256 c14ecad52243b27289d434d62f3b64b235dea47f461385dfd126b93932822d04
MD5 c0983532365c51a885b1eaf79d7a47a9
BLAKE2b-256 e5adfce295c6d6de95bf0fbd41fd033bae6564eb5cbae885f21c25b9fb33f5a3

See more details on using hashes here.

File details

Details for the file diwire-1.0.2-py3-none-any.whl.

File metadata

  • Download URL: diwire-1.0.2-py3-none-any.whl
  • Upload date:
  • Size: 86.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for diwire-1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 71591d905f3e0c55ff6cae813b046486ee66817d5e21828a683b1eb9fb3075f0
MD5 55adfcd0d5a3808d710e51a87f086561
BLAKE2b-256 fcefa56bbe7b75414f00b18da70b26b90b40585515cc20a8d65d8e5e6df14092

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page