Skip to main content

Python Dependency Injection Library

Project description

Wireup

Type-driven dependency injection for Python. Wireup is battle-tested in production, thread-safe, no-GIL (PEP 703) ready, and fail-fast by design: if the container starts, it works.

GitHub GitHub Workflow Status (with event) PyPI - Python Version PyPI - Version

Quick Start · Docs · Benchmarks · Migrate to Wireup

Why Wireup?

🔁 Define Once, Use Anywhere

Reuse the same application code in APIs, CLIs, workers, and scripts without rewriting your wiring.

✅ Correct by Default

If the container starts, your dependency graph is valid. Wireup checks for missing or misconfigured dependencies to avoid surprises at runtime. See What Wireup Validates

🌐 Framework-Ready

Native integrations for FastAPI, Django, Flask, Starlette, Celery, Click, Typer, and more.

⚡ Zero-Overhead Handlers

Resolve singleton constructor dependencies once at startup in FastAPI and AIOHTTP class-based handlers, not per request.

🧩 Advanced Wiring

Go beyond simple constructor injection with reusable bundles, explicit scope context sharing, and more using plain Python.

🧪 Easy to test

Override dependencies with context managers, keep tests isolated, and restore the original graph automatically.

Benchmarks

Scoped Dependency Injection Performance

Dense dependency graph resolved per request in FastAPI + Uvicorn
(Requests per second, higher is better. Manual Wiring represents the upper bound.)
Full methodology and reproducibility: benchmarks.

Installation

pip install wireup

Quick Start

import fastapi
import wireup
import wireup.integration.fastapi
from wireup import Injected, injectable


@injectable
class Database:
    def query(self, sql: str) -> list[str]: ...


@injectable
class UserService:
    def __init__(self, db: Database) -> None:
        self.db = db

    def get_users(self) -> list[str]:
        return self.db.query("SELECT name FROM users")


app = fastapi.FastAPI()


@app.get("/users")
def get_users(service: Injected[UserService]) -> list[str]:
    return service.get_users()


container = wireup.create_async_container(injectables=[Database, UserService])
wireup.integration.fastapi.setup(container, app)

For a full end-to-end walkthrough, start with the Getting Started guide.

Basic Usage

Wireup also supports config injection, decorator-free domain models, and package-level registration.

1. Inject Configuration

Inject configuration alongside dependencies. No need to write factories just to pass a config value.

@injectable
class Database:
    def __init__(self, url: Annotated[str, Inject(config="db_url")]) -> None:
        self.engine = sqlalchemy.create_engine(url)

container = wireup.create_sync_container(
    injectables=[Database],
    config={"db_url": os.environ["DB_URL"]}
)

2. Clean Architecture

Need strict boundaries? Use factories to wire pure domain objects and integrate external libraries like Pydantic.

# 1. No Wireup imports
class Database:
    def __init__(self, url: str) -> None:
        self.engine = create_engine(url)

# 2. Configuration (Pydantic)
class Settings(BaseModel):
    db_url: str = "sqlite://"
# 3. Wireup factories
@injectable
def make_settings() -> Settings:
    return Settings()

@injectable
def make_database(settings: Settings) -> Database:
    return Database(url=settings.db_url)

container = wireup.create_sync_container(injectables=[make_settings, make_database])

3. Package-level registration

No need to list every injectable manually. Provide entire modules or packages to register all at once.

import app
import wireup

container = wireup.create_sync_container(
    injectables=[
        app.services,
        app.repositories,
        app.factories
    ]
)

More Features

🎯 Function Injection

Inject dependencies into CLI commands, background tasks, event handlers, or any standalone function that needs container access.

@inject_from_container(container)
def migrate_database(db: Injected[Database], settings: Injected[Settings]) -> None:
    ...

📝 Interfaces & Abstractions

Bind implementations to interfaces using Protocols or ABCs.

class Notifier(Protocol):
    def notify(self) -> None: ...

@injectable(as_type=Notifier)
class SlackNotifier:
    def notify(self) -> None: ...

# SlackNotifier is injected wherever Notifier is requested
@app.post("/notify")
def send_notification(notifier: Injected[Notifier]) -> None:
    notifier.notify()

🏭 Factories & Resources

Defer instantiation to specialized factories when complex initialization or cleanup is required. Full support for sync, async, and generator factories. Wireup handles cleanup at the right time based on lifetime.

class WeatherClient:
    def __init__(self, client: requests.Session) -> None:
        self.client = client

@injectable
def weather_client_factory() -> Iterator[WeatherClient]:
    with requests.Session() as session:
        yield WeatherClient(client=session)
Async example
class WeatherClient:
    def __init__(self, client: aiohttp.ClientSession) -> None:
        self.client = client

@injectable
async def weather_client_factory() -> AsyncIterator[WeatherClient]:
    async with aiohttp.ClientSession() as session:
        yield WeatherClient(client=session)

🔄 Lifetimes & Scopes

Declare dependencies as singleton, scoped, or transient to control reuse explicitly.

# Singleton: one instance per application (default)
@injectable
class Settings:
    pass

# Async singleton with cleanup — no lru_cache, no app.state
@injectable
async def database_factory(settings: Settings) -> AsyncIterator[AsyncConnection]:
    async with create_async_engine(settings.db_url).connect() as connection:
        yield connection

# Scoped: one instance per request, shared within that request
@injectable(lifetime="scoped")
class RequestContext:
    def __init__(self) -> None:
        self.request_id = uuid4()

# Transient: fresh instance every time
@injectable(lifetime="transient")
class OrderProcessor:
    pass

🛡️ Startup Validation

Wireup validates the dependency graph when the container is created. See What Wireup Validates for the full rules and limits.

# Missing dependencies: caught at startup, not at runtime
@injectable
class Foo:
    def __init__(self, unknown: NotManagedByWireup) -> None: ...

container = wireup.create_sync_container(injectables=[Foo])
# ❌ Parameter 'unknown' of 'Foo' depends on an unknown injectable 'NotManagedByWireup'.

It also catches circular dependencies, duplicate registrations, misconfigured lifetimes, and missing config at startup.

🧪 Testing

Wireup decorators only collect metadata. Injectables are plain classes and functions, so you can test them directly with no special setup.

Swap dependencies during tests with container.override:

with container.override.injectable(target=Database, new=in_memory_database):
    # Injectables that depend on Database will receive in_memory_database
    # for the duration of this context manager
    response = client.get("/users")

📚 Documentation

See the docs for integrations, lifetimes, factories, testing, and more advanced patterns.

https://maldoinc.github.io/wireup

If Wireup is useful to you, a star on GitHub helps others find it.

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

wireup-2.10.0.tar.gz (791.1 kB view details)

Uploaded Source

Built Distribution

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

wireup-2.10.0-py3-none-any.whl (58.1 kB view details)

Uploaded Python 3

File details

Details for the file wireup-2.10.0.tar.gz.

File metadata

  • Download URL: wireup-2.10.0.tar.gz
  • Upload date:
  • Size: 791.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"43","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for wireup-2.10.0.tar.gz
Algorithm Hash digest
SHA256 0913de3002872b8e68848d9ea7ab7bc45c2c4bf5a63c6fc35e181df286082a2e
MD5 41c049f19a4a743491c0ac72fa274f52
BLAKE2b-256 eea02b4c04b030ba22bb5b75ffc2b8d077b7cb5635397160070551df2d05d3cf

See more details on using hashes here.

File details

Details for the file wireup-2.10.0-py3-none-any.whl.

File metadata

  • Download URL: wireup-2.10.0-py3-none-any.whl
  • Upload date:
  • Size: 58.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.26 {"installer":{"name":"uv","version":"0.9.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"43","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for wireup-2.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 252368a254010986ee89919087f9610c44f8fdf4753961bbb60d242dadbdfe5f
MD5 42da1b8f6410f7d4d3d79e0b50f3375b
BLAKE2b-256 2ad483d79633b96f9784a1f0c1ffa728aaf05e9760dc781158c1f00292600081

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