Skip to main content

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

Project description

diwire - dependency injection for Python

Type-safe dependency injection with automatic wiring, scoped lifetimes, and async-safe factories.

PyPI version Python versions License: MIT codecov

diwire is a lightweight DI container for Python 3.10+ that resolves dependency graphs from type hints, supports scoped lifetimes, and cleans up resources via generator factories. It is async-first, thread-safe, and has zero runtime dependencies.

Why diwire

  • Automatic wiring from type hints (constructor and function injection)
  • Scoped lifetimes for request/session workflows
  • Generator factories with cleanup on scope exit
  • Async support with aresolve() and async factories
  • Interface + component registration for multiple implementations
  • Zero dependencies and minimal overhead

Installation

uv add diwire
pip install diwire

Quick start

from dataclasses import dataclass

from diwire import Container, Lifetime


@dataclass
class Database:
    host: str = "localhost"


@dataclass
class UserRepository:
    db: Database


@dataclass
class UserService:
    repo: UserRepository


container = Container(autoregister_default_lifetime=Lifetime.TRANSIENT)
service = container.resolve(UserService)

print(service.repo.db.host)

Registering services

You can register classes, factories, or instances. concrete_class lets you register by interface or abstract base class.

from dataclasses import dataclass
from datetime import datetime
from typing import Protocol

from diwire import Container, Lifetime


class Clock(Protocol):
    def now(self) -> str: ...


@dataclass
class SystemClock:
    def now(self) -> str:
        return datetime.now().isoformat(timespec="seconds")


container = Container()
container.register(Clock, concrete_class=SystemClock, lifetime=Lifetime.SINGLETON)
clock = container.resolve(Clock)

Open generics

Register open generic factories and resolve closed generics with type-safe validation. Type arguments can be injected by annotating parameters as type[T], and TypeVar bounds/constraints are enforced at resolution time.

from dataclasses import dataclass
from typing import Generic, TypeVar

from diwire import Container


class Model:
    pass


T = TypeVar("T")
M = TypeVar("M", bound=Model)


@dataclass
class AnyBox(Generic[T]):
    value: str


@dataclass
class ModelBox(Generic[M]):
    model: M


container = Container()


@container.register(AnyBox[T])
def create_any_box(type_arg: type[T]) -> AnyBox[T]:
    return AnyBox(value=type_arg.__name__)


@container.register(ModelBox[M])
def create_model_box(model_cls: type[M]) -> ModelBox[M]:
    return ModelBox(model=model_cls())


print(container.resolve(AnyBox[int]))
print(container.resolve(ModelBox[Model]))

Function injection

Mark parameters with FromDI() to inject dependencies while keeping other parameters caller-provided.

from dataclasses import dataclass
from typing import Annotated

from diwire import Container, FromDI


@dataclass
class EmailService:
    smtp_host: str = "smtp.example.com"

    def send(self, to: str, subject: str) -> str:
        return f"Sent '{subject}' to {to} via {self.smtp_host}"


def send_email(
    to: str,
    *,
    mailer: Annotated[EmailService, FromDI()],
) -> str:
    return mailer.send(to, "Hello!")


container = Container()
send = container.resolve(send_email)
print(send(to="user@example.com"))

Scopes and cleanup

Use scopes to manage request/session lifetimes. Generator factories clean up automatically.

from collections.abc import Generator

from diwire import Container, Lifetime


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.register(Session, factory=session_factory, lifetime=Lifetime.SCOPED_SINGLETON, scope="request")

with container.start_scope("request") as scope:
    session = scope.resolve(Session)
    assert session.closed is False

Named components

Use Component and ServiceKey to register multiple implementations of the same interface.

from dataclasses import dataclass
from typing import Annotated, Protocol

from diwire import Container
from diwire.service_key import Component


class Cache(Protocol):
    def get(self, key: str) -> str: ...


@dataclass
class RedisCache:
    def get(self, key: str) -> str:
        return f"redis:{key}"


@dataclass
class MemoryCache:
    def get(self, key: str) -> str:
        return f"memory:{key}"


container = Container()
container.register(Annotated[Cache, Component("primary")], instance=RedisCache())
container.register(Annotated[Cache, Component("fallback")], instance=MemoryCache())

primary: Cache = container.resolve(Annotated[Cache, Component("primary")])
fallback: Cache = container.resolve(Annotated[Cache, Component("fallback")])

Async support

Use aresolve() with async factories and async generator cleanup.

import asyncio
from collections.abc import AsyncGenerator

from diwire import Container, Lifetime


class AsyncClient:
    async def close(self) -> None: ...


async def client_factory() -> AsyncGenerator[AsyncClient, None]:
    client = AsyncClient()
    try:
        yield client
    finally:
        await client.close()


async def main() -> None:
    container = Container()
    container.register(
        AsyncClient,
        factory=client_factory,
        lifetime=Lifetime.SCOPED_SINGLETON,
        scope="request",
    )

    async with container.start_scope("request") as scope:
        await scope.aresolve(AsyncClient)


asyncio.run(main())

Global container context

For larger apps, container_context provides a context-local global container.

from dataclasses import dataclass
from typing import Annotated

from diwire import Container, FromDI, container_context


@container_context.register()
@dataclass
class Service:
    name: str = "diwire"


@container_context.resolve()
def greet(service: Annotated[Service, FromDI()]) -> str:
    return f"hello {service.name}"


container = Container()
container_context.set_current(container)


print(greet())

API at a glance

  • Container: register, resolve, aresolve, start_scope, compile
  • Lifetime: TRANSIENT, SINGLETON, SCOPED_SINGLETON
  • FromDI: Annotated[T, FromDI()] parameter marker
  • container_context: context-local global container
  • Component and ServiceKey: named registrations

Performance

Container.compile() precompiles providers to reduce reflection and dict lookups. By default, the container auto-compiles on first resolve (set auto_compile=False to disable) and auto-registers constructor-injected types using autoregister_default_lifetime.

Examples

See examples/README.md for a guided tour of patterns, async usage, FastAPI-style integration, and error handling.

License

MIT

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-0.0.4.tar.gz (209.3 kB view details)

Uploaded Source

Built Distribution

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

diwire-0.0.4-py3-none-any.whl (39.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: diwire-0.0.4.tar.gz
  • Upload date:
  • Size: 209.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","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-0.0.4.tar.gz
Algorithm Hash digest
SHA256 c12e2582c76227f52cac95de3d6142f57b28cd5e7b20b1ceeff3b8221901c6e4
MD5 cc20566278c344b1061f0b0ec63e5bcd
BLAKE2b-256 76a2121f9c526f7ef20fd64c46fecc6a7dda53d0a7a7d9eae9b2b88ac0376b83

See more details on using hashes here.

File details

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

File metadata

  • Download URL: diwire-0.0.4-py3-none-any.whl
  • Upload date:
  • Size: 39.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","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-0.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 812a684abce2960e6bcd6fc47704770aec45b8b0301674364f75eeca915d8008
MD5 7450c932b383ebfa3e32a031e23c016f
BLAKE2b-256 f9c80c917efa89800b86596fdffd82dc0af461c0c0b3641121bc3ce6badccbb5

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