Skip to content

Injectables

An injectable is any class or function that you register with the container, making it available to be requested as a dependency. Once registered, Wireup can instantiate it, resolve its own dependencies, and inject it wherever needed.

The @injectable Decorator

The @injectable decorator marks a class or function for registration with the container.

from wireup import injectable


@injectable
class UserRepository: ...
from wireup import injectable


@injectable
def db_connection() -> DatabaseConnection: ...

Arguments

You can customize how an injectable is registered by passing arguments to the decorator:

Argument Description Default
lifetime Controls how long the object lives (e.g., "singleton", "scoped"). See Lifetimes. "singleton"
qualifier A unique identifier, useful when you have multiple implementations of the same type. See Interfaces. None
as_type Register the object as a different type (like a Protocol or Base Class). See Interfaces. None
from wireup import injectable


@injectable(lifetime="scoped", qualifier="readonly")
class DbSession: ...

Defining Dependencies

Wireup resolves dependencies using Type Hints. It inspects the types you declare and automatically finds the matching injectable.

Classes

Standard Python classes with type-hinted __init__ methods are automatically wired. No extra configuration is needed.

from wireup import injectable


@injectable
class UserService:
    # UserRepository will be injected automatically
    def __init__(self, repo: UserRepository) -> None:
        self.repo = repo

Factories

Functions can be registered as factories. This is standard for creating 3rd-party objects, when complex setup is required or for enforcing clean architecture.

See Factories and Resource Management.

import boto3
from wireup import injectable, Inject
from typing import Annotated


@injectable
def create_s3_client(
    region: Annotated[str, Inject(config="aws_region")],
) -> boto3.client:
    return boto3.client("s3", region_name=region)

Dataclasses

You can combine @injectable with @dataclass to eliminate __init__ boilerplate.

@injectable
class OrderProcessor:
    def __init__(
        self,
        payment_gateway: PaymentGateway,
        inventory_service: InventoryService,
    ):
        self.payment_gateway = payment_gateway
        self.inventory_service = inventory_service
from dataclasses import dataclass


@injectable
@dataclass
class OrderProcessor:
    payment_gateway: PaymentGateway
    inventory_service: InventoryService
Counter-example

Mix with caution if your class has many non-dependency fields.

@injectable
@dataclass
class Foo:
    FOO_CONST = 1  # Not added to __init__ by @dataclass.
    logger = logging.getLogger(__name__)  # Not added to __init__ by @dataclass.

    # These will be added to __init__ by @dataclass
    # and marked as dependencies by Wireup.
    payment_gateway: PaymentGateway
    inventory_service: InventoryService
    order_repository: OrderRepository

In this example, due to how the @dataclass decorator works, combining the two leads to code that's more difficult to read, since it's not immediately obvious what are dependencies and what are class fields.

Optional Dependencies and Default Values

Wireup thinks about constructor parameters in terms of satisfiability: can this parameter be provided by either Wireup or by Python itself?

When Wireup encounters a dependency it doesn't recognize, it normally raises an error. However, if that parameter has an explicit default value, Wireup will skip it and let Python use the default instead.

This means there are two different ways an optional-looking dependency can work.

flowchart LR
    A[Parameter] --> B{Can Wireup provide it?}
    B -->|Yes| C[Inject dependency]
    B -->|No| D{Has default value?}
    D -->|Yes| E[Use Python default]
    D -->|No| F[Raise error]

1. Optional by Default Value

If a parameter has a default value, Wireup will still try to provide it first. The default is only used when Wireup does not know how to provide that dependency:

from wireup import injectable


@injectable
class UserService:
    def __init__(self, cache: Redis | None = None) -> None:
        self.cache = cache

This is valid even if Redis is not registered, because the constructor is still satisfiable without Wireup creating the dependency. If Redis is registered, Wireup provides it instead of using the default.

This is also useful when integrating with libraries that add their own __init__ parameters, such as Pydantic Settings:

from pydantic_settings import BaseSettings
from wireup import injectable


@injectable
class Settings(BaseSettings):
    app_name: str = "myapp"
    debug: bool = False

In this example, Pydantic's BaseSettings adds parameters that Wireup doesn't manage. Since they have defaults, Wireup allows the class to be registered without errors.

2. Optional by Factory

If there is no default value, Wireup still needs a registered way to satisfy the parameter. For dependencies where None is a valid result, register a factory that returns T | None:

from wireup import injectable


@injectable
def cache_factory(settings: Settings) -> Redis | None:
    if not settings.use_cache:
        return None

    return Redis.from_url(settings.redis_url)


@injectable
class UserService:
    def __init__(self, cache: Redis | None) -> None:
        self.cache = cache

In this case the dependency is optional at runtime, but it is still explicitly part of the dependency graph because Wireup has a registered way to resolve Redis | None.

When you register a dependency this way, you should also request it as Redis | None (or Optional[Redis]).

See Factories: Optional Dependencies and Interfaces: as_type with Optional Types.

Existing Instances

If you already have an object and want to make it injectable, use wireup.instance(...).

import wireup


settings = AppSettings()

container = wireup.create_sync_container(
    injectables=[
        wireup.instance(settings, as_type=AppSettings),
    ]
)

This is useful when an object is created by another library, built manually during startup, or should be shared as a prebuilt singleton.

wireup.instance(...) also supports qualifier and as_type, so you can expose the object under an interface or register more than one instance of the same type.

container = wireup.create_sync_container(
    injectables=[
        wireup.instance(primary_db, as_type=Database),
        wireup.instance(analytics_db, as_type=Database, qualifier="analytics"),
    ]
)

Next Steps