Basics

Registration methods

Demonstrates three ways to register services:

  1. Class registration - container creates instances

  2. Factory registration - custom function creates instances

  3. Instance registration - pre-created singleton

Class registration

from diwire import Container


class Logger:
    def log(self, message: str) -> None:
        print(f"[LOG] {message}")


def main() -> None:
    container = Container(autoregister=False)
    container.register(Logger)

    logger = container.resolve(Logger)
    logger.log("Hello from registered class!")


if __name__ == "__main__":
    main()

Factory registration

from dataclasses import dataclass

from diwire import Container


@dataclass
class Database:
    host: str
    port: int


def create_database() -> Database:
    # Use a factory when you need custom instantiation logic.
    return Database(host="localhost", port=5432)


def main() -> None:
    container = Container(autoregister=False)
    container.register(Database, factory=create_database)

    db = container.resolve(Database)
    print(f"Database: {db.host}:{db.port}")


if __name__ == "__main__":
    main()

Instance registration

from diwire import Container


class Cache:
    def __init__(self) -> None:
        self.data: dict[str, str] = {}


def main() -> None:
    container = Container(autoregister=False)

    # Instance registrations are always singletons.
    cache_instance = Cache()
    cache_instance.data["key"] = "value"
    container.register(Cache, instance=cache_instance)

    resolved_cache = container.resolve(Cache)
    print(f"Cache data: {resolved_cache.data}")
    print(f"Same instance: {resolved_cache is cache_instance}")


if __name__ == "__main__":
    main()

Lifetimes (TRANSIENT vs SINGLETON)

Demonstrates the difference between:

  • TRANSIENT: new instance on every resolve

  • SINGLETON: same instance for entire container lifetime

from diwire import Container, Lifetime


class TransientService:
    """Created fresh on each resolution."""


class SingletonService:
    """Shared across all resolutions."""


def main() -> None:
    container = Container(autoregister=False)

    # TRANSIENT: new instance every time
    container.register(TransientService, lifetime=Lifetime.TRANSIENT)

    t1 = container.resolve(TransientService)
    t2 = container.resolve(TransientService)
    t3 = container.resolve(TransientService)

    print("TRANSIENT instances:")
    print(f"  t1 id: {id(t1)}")
    print(f"  t2 id: {id(t2)}")
    print(f"  t3 id: {id(t3)}")
    print(f"  All different: {t1 is not t2 is not t3}")

    # SINGLETON: same instance always
    container.register(SingletonService, lifetime=Lifetime.SINGLETON)

    s1 = container.resolve(SingletonService)
    s2 = container.resolve(SingletonService)
    s3 = container.resolve(SingletonService)

    print("\nSINGLETON instances:")
    print(f"  s1 id: {id(s1)}")
    print(f"  s2 id: {id(s2)}")
    print(f"  s3 id: {id(s3)}")
    print(f"  All same: {s1 is s2 is s3}")


if __name__ == "__main__":
    main()

Constructor injection (auto-wiring)

Demonstrates automatic dependency resolution through constructor parameters. The container analyzes type hints and injects dependencies automatically.

from dataclasses import dataclass
from typing import Any

from diwire import Container


@dataclass
class Config:
    """Application configuration."""

    database_url: str = "postgresql://localhost/app"
    debug: bool = True


@dataclass
class Database:
    """Database connection that depends on Config."""

    config: Config

    def query(self, sql: str, **kwargs: Any) -> str:
        return f"Executing on {self.config.database_url}: {sql.format(**kwargs)}"


@dataclass
class UserRepository:
    """Repository that depends on Database."""

    db: Database

    def find_user(self, user_id: int) -> str:
        return self.db.query("SELECT * FROM users WHERE id = {user_id}", user_id=user_id)


@dataclass
class UserService:
    """Service that depends on UserRepository."""

    repo: UserRepository

    def get_user_info(self, user_id: int) -> str:
        return f"User info: {self.repo.find_user(user_id)}"


def main() -> None:
    container = Container()

    # Register Config with a specific instance
    container.register(Config, instance=Config(database_url="postgresql://prod/app"))

    # Resolve UserService - container automatically resolves entire chain:
    # UserService -> UserRepository -> Database -> Config
    service = container.resolve(UserService)

    result = service.get_user_info(42)
    print(result)

    # The entire dependency chain was resolved:
    print("\nDependency chain resolved:")
    print(f"  UserService has repo: {service.repo}")
    print(f"  UserRepository has db: {service.repo.db}")
    print(f"  Database has config: {service.repo.db.config}")


if __name__ == "__main__":
    main()

Decorator registration

@container.register works as a decorator for:

  • classes (bare decorator or with explicit lifetime/scope)

  • factory functions (return type inferred from annotations)

  • interface / protocol bindings (@container.register(Protocol, ...))

More scoped patterns and cleanup are in Scopes. Async factories and async generator cleanup are in Async. container_context is covered in container_context and Patterns.

Class decorators

from diwire import Container, Lifetime

container = Container(autoregister=False)


@container.register
class Config:
    def __init__(self) -> None:
        self.debug = True


@container.register(lifetime=Lifetime.SINGLETON)
class Logger:
    def log(self, message: str) -> None:
        print(f"[LOG] {message}")


def main() -> None:
    cfg = container.resolve(Config)
    print(f"Config.debug={cfg.debug}")

    logger1 = container.resolve(Logger)
    logger2 = container.resolve(Logger)
    logger1.log(f"Logger is singleton: {logger1 is logger2}")


if __name__ == "__main__":
    main()

Factory function decorators

from dataclasses import dataclass

from diwire import Container

container = Container(autoregister=False)


@dataclass
class Cache:
    data: dict[str, str]


@container.register
def create_cache() -> Cache:
    # The key is inferred from the return annotation.
    return Cache(data={"initialized": "true"})


def main() -> None:
    cache = container.resolve(Cache)
    print(cache)


if __name__ == "__main__":
    main()

Factory decorators with injected dependencies

from dataclasses import dataclass

from diwire import Container

container = Container(autoregister=False)


@dataclass
class Settings:
    env: str


@dataclass
class Service:
    settings: Settings


container.register(Settings, instance=Settings(env="dev"))


@container.register
def create_service(settings: Settings) -> Service:
    # Factory params are injected from type hints.
    return Service(settings=settings)


def main() -> None:
    service = container.resolve(Service)
    print(f"Service.settings.env={service.settings.env}")


if __name__ == "__main__":
    main()

Protocol/interface binding

from typing import Protocol

from diwire import Container, Lifetime


class Database(Protocol):
    def query(self, sql: str) -> str: ...


container = Container(autoregister=False)


@container.register(Database, lifetime=Lifetime.SINGLETON)
class PostgresDatabase:
    def query(self, sql: str) -> str:
        return f"[Postgres] {sql}"


def main() -> None:
    db = container.resolve(Database)
    print(db.query("SELECT 1"))


if __name__ == "__main__":
    main()

Open generics

Open generics let you register a single factory for a generic type (Box[T]) and resolve it for any concrete type argument (Box[int], Box[str], …).

from dataclasses import dataclass
from typing import Generic, TypeVar

from diwire import Container

T = TypeVar("T")


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


container = Container()

@container.register(Box[T])
def create_box(type_arg: type[T]) -> Box[T]:
    return Box(value=f"Box[{type_arg.__name__}]")


print(container.resolve(Box[int]))
print(container.resolve(Box[str]))

TypeVar bounds validation

diwire validates TypeVar bounds/constraints at runtime.

from dataclasses import dataclass
from typing import Generic, TypeVar

from diwire import Container
from diwire.exceptions import DIWireInvalidGenericTypeArgumentError


class Model:
    pass


class User(Model):
    pass


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


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


container = Container()

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


print(container.resolve(ModelBox[User]))
try:
    container.resolve(ModelBox[str])
except DIWireInvalidGenericTypeArgumentError as e:
    print(f"Caught: {type(e).__name__}")
    print(f"Message: {e}")

Compilation

Demonstrates:

  • manual compilation via container.compile()

  • disabling auto-compilation via Container(auto_compile=False)

from dataclasses import dataclass

from diwire import Container, Lifetime


@dataclass
class ServiceA:
    value: str = "A"


@dataclass
class ServiceB:
    a: ServiceA


def main() -> None:
    # Turn off auto-compilation so we can show the explicit call.
    container = Container(auto_compile=False)

    container.register(ServiceA, lifetime=Lifetime.SINGLETON)
    container.register(ServiceB, lifetime=Lifetime.TRANSIENT)

    # Works before compilation (reflection-based resolution).
    b1 = container.resolve(ServiceB)
    print(f"Before compile(): b1.a.value={b1.a.value!r}")

    # Precompute the dependency graph for maximum throughput.
    container.compile()

    b2 = container.resolve(ServiceB)
    print(f"After compile():  b2.a.value={b2.a.value!r}")

    # Transient behavior is unchanged: new ServiceB each time.
    print(f"Transient preserved: {b1 is not b2}")

    # Singleton behavior is unchanged: same ServiceA.
    print(f"Singleton preserved: {b1.a is b2.a}")


if __name__ == "__main__":
    main()

Read more