Skip to main content

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.

Frameworks & integrations

Installation

uv add diwire

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] for ergonomic handlers. (Function injection)
  • Framework/task support: request/job scope patterns for FastAPI, Litestar, aiohttp, Starlette, Flask, Django, and Celery. (Integrations)
  • 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.6 (Apple M3 Pro, strict mode):

  • diwire is the top performer across this suite, reaching up to 6.30× vs rodi, 7.50× vs dishka, and 5.01× vs wireup.
  • Resolve-only comparisons (scope-capable libraries): diwire reaches up to 3.53× (rodi), 4.42× (dishka), and 4.38× (wireup).
  • Current benchmark totals: 17 full-suite scenarios and 9 resolve-only scenarios.

For quick local regression checks, run make benchmark (diwire-only). For full cross-library runs, use make benchmark-comparison (raw suite) or make benchmark-report / make benchmark-report-resolve (report artifacts).

Quick start (pure Python 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

When provider setup needs injected state of its own, register a callable class instead of a function. The class constructor receives dependencies, and the instance __call__ produces the dependency value.

from dataclasses import dataclass

from diwire import Container, Injected


@dataclass(frozen=True)
class Settings:
    endpoint: str


class Client:
    def __init__(self, endpoint: str) -> None:
        self.endpoint = endpoint


@dataclass(kw_only=True)
class ClientFactory:
    settings: Injected[Settings]

    def __call__(self) -> Client:
        return Client(self.settings.endpoint)


container = Container()
container.add_instance(Settings(endpoint="https://api.example.test"))
container.add_factory_class(ClientFactory, provides=Client)

print(container.resolve(Client).endpoint)  # => https://api.example.test

The same pattern is available for cleanup providers: use add_generator_class() when __call__ yields the resource, or add_context_manager_class() when __call__ returns a context manager.

Scopes & cleanup

Use Lifetime.SCOPED for per-request/per-job caching. Use generator/async-generator providers for deterministic cleanup on scope exit. If the provider needs constructor-injected state, use the class variants and put the cleanup logic in __call__.

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

Static typing note: without a checker plugin, injected wrappers keep return types but accept permissive arguments. For precise mypy signatures (optional injected params, strict non-injected params, optional diwire_resolver kwarg), enable:

[tool.mypy]
plugins = ["diwire.integrations.mypy_plugin"]

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 contextvars import ContextVar

from diwire import Container, Injected, Scope, resolver_context

current_user_id_var: ContextVar[int] = ContextVar("current_user_id", default=0)


def read_current_user_id() -> int:
    return current_user_id_var.get()


container = Container()
container.add_factory(read_current_user_id, provides=int, scope=Scope.REQUEST)


@resolver_context.inject(scope=Scope.REQUEST)
def handler(value: Injected[int]) -> int:
    return value


with container.enter_scope(Scope.REQUEST) as request_scope:
    token = current_user_id_var.set(7)
    try:
        print(handler(diwire_resolver=request_scope))  # => 7
    finally:
        current_user_id_var.reset(token)

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.

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.4.3.tar.gz (522.2 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.4.3-py3-none-any.whl (104.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: diwire-1.4.3.tar.gz
  • Upload date:
  • Size: 522.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","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.4.3.tar.gz
Algorithm Hash digest
SHA256 040930f7d96693b7b780ad376ef9c92b0a20784588b3f577adbd3cd0ef68d11a
MD5 7619314879cf44f85fd9e3903eea19d4
BLAKE2b-256 1f718e7d69501c249531903145e22d61754b8c70bf3b8e6fb2567723fd31e410

See more details on using hashes here.

File details

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

File metadata

  • Download URL: diwire-1.4.3-py3-none-any.whl
  • Upload date:
  • Size: 104.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","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.4.3-py3-none-any.whl
Algorithm Hash digest
SHA256 274184f07e986646c48c3f1b7c10b55cf2052e54181a64ed0d7601e62097de2c
MD5 71570a80214f591bb61670fce6b03d5f
BLAKE2b-256 18583990013e6f5b458b4732a737d6c5a3ee40b4c76a517f9d2bb93c62dd2da9

See more details on using hashes here.

Release history Release notifications | RSS feed

1.4.4

2 files

This release

1.4.3 This release

2 files

1.4.2

2 files

1.4.1

2 files

1.4.0

2 files

1.3.2

2 files

1.3.1

2 files

1.3.0

2 files

1.2.0

2 files

1.1.0

2 files

1.0.2

2 files

1.0.1

2 files

1.0.0

2 files

0.1.0

2 files

0.0.9

2 files

0.0.8

2 files

0.0.7

2 files

0.0.6

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.2

2 files

0.0.1

2 files

Supported by

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