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
Documentation Documentation

Scoped Dependency Injection Performance

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

[!TIP] New: Inject singleton dependencies in FastAPI with zero overhead using Class-Based Handlers.

Why Wireup?

  • Correct by default: Wireup catches missing dependencies, circular references, lifetime mismatches, duplicate registrations, and missing config keys at startup. Shared dependencies are created in a thread-safe way.
  • Define once, inject anywhere: reuse the same service layer in APIs, CLIs, workers, and scripts.
  • Framework-ready: native integrations for FastAPI, Flask, Django, Starlette, AIOHTTP, ASGI, FastMCP, Celery, Click, Typer, and Strawberry. See Integrations.
  • Startup-resolved constructor injection for FastAPI and AIOHTTP handlers: constructor dependencies are resolved once at startup, not per request. FastAPI class-based handlers.
  • Test overrides with context managers: replace any injectable for a test scope and restore automatically. See testing docs.
  • Reusable sub-graphs: run multiple configured instances of the same dependency graph without spinning up separate containers. See reusable factory bundles.

Installation

pip install wireup

Complete Example

@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 fully working end-to-end walkthrough, see the Getting Started guide.

Features

⚡ Clean & Type-Safe DI

Use decorators and annotations for concise, co-located definitions, or factories to keep your domain model pure and decoupled.

1. Basic Usage

Register classes with @injectable and let the container resolve dependencies automatically.

@injectable
class Database:
    def __init__(self) -> None:
        self.engine = sqlalchemy.create_engine("sqlite://")

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

container = wireup.create_sync_container(injectables=[Database, UserService])

# Inject via framework integration or @inject_from_container (recommended)
@app.get("/users")
def get_users(service: Injected[UserService]) -> list[str]: ...

# Or resolve directly for advanced use cases (middleware, startup, scripts)
user_service = container.get(UserService)

2. Inject Configuration

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

View Code
@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"]}
)

3. 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])

4. Auto-Discover

No need to list every injectable manually. Scan entire modules or packages to register all at once. This is the recommended default for larger applications.

View Code
import app
import wireup

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

🎯 Function Injection

Inject dependencies into any function. 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

Depend on abstractions, not implementations. 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: ...

container = create_sync_container(injectables=[SlackNotifier])

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

🏭 Flexible Creation Patterns

Defer instantiation to factories when initialization or cleanup is non-trivial. 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)

🔄 Managed Lifetimes

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

# 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

❓ Optional Dependencies

First-class support for Optional[T] and T | None.

@injectable
def make_cache(settings: Settings) -> RedisCache | None:
    return RedisCache(settings.redis_url) if settings.cache_enabled else None

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

# Retrieve optional dependencies directly when needed
cache = container.get(RedisCache | None)

🧩 Reusable sub-graphs

Need to register multiple sub-graphs with different settings (e.g. primary + analytics DB)?

Wireup supports this natively without requiring a dedicated provider class or a separate container. See Reusable Factory Bundles.

🛡️ Startup Validation

Wireup validates the entire dependency graph when the container is created.

# 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'.
container = wireup.create_sync_container(injectables=[])

# Decorated functions validated at import time
@inject_from_container(container)
def my_function(oops: Injected[NotManagedByWireup]): ...
# ❌ Parameter 'oops' of 'my_function' depends on an unknown injectable 'NotManagedByWireup'.
# Missing config keys caught at startup
@injectable
class Database:
    def __init__(self, url: Annotated[str, Inject(config="db_url")]) -> None: ...

container = wireup.create_sync_container(injectables=[Database], config={})
# ❌ Parameter 'url' of Type 'Database' depends on an unknown Wireup config key 'db_url'.

Additional checks: circular dependencies, lifetime mismatches (e.g. singleton depending on scoped), and duplicate registrations.

📍 Framework Independent

Define your service layer once. Run it anywhere.

# Define once
# injectables = [UserService, Database, ...]

# FastAPI (native integration, no extra decorator needed)
@app.get("/users")
async def view(service: Injected[UserService]): ...

# Click
@click.command()
def command(service: Injected[UserService]): ...

# Use @inject_from_container to inject dependencies into any function.
# Most useful for scripts or when no Wireup integration is available.
@inject_from_container(container)
def run_worker(service: Injected[UserService]): ...

Have a useful integration to recommend? Create an issue or PR!

🔌 Framework Integrations

Native integrations manage request scopes, endpoint injection, and dependency lifetimes.

Supported: FastAPI, Flask, Django, AIOHTTP, Starlette, Click, Typer, Strawberry

View all integrations →

🧪 Simplified Testing

Wireup decorators only collect metadata. Injectables are plain classes and functions. Test them directly with no special setup.

Swap dependencies during tests with container.override:

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

📚 Documentation

https://maldoinc.github.io/wireup

If Wireup helps your team move faster, consider starring the repo to help more Python developers discover 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.8.0.tar.gz (739.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.8.0-py3-none-any.whl (56.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: wireup-2.8.0.tar.gz
  • Upload date:
  • Size: 739.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.8.0.tar.gz
Algorithm Hash digest
SHA256 979d60176c41585702b5cc1081dffb8c07b9f97ca43f9443a675857b68b37017
MD5 2e328a30886c0fa7fb7f0a163f96d959
BLAKE2b-256 abe7ca941adf6bb1cdcaf2e9510c6e57c9a2495ac93c4adc978e19d2e62e4225

See more details on using hashes here.

File details

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

File metadata

  • Download URL: wireup-2.8.0-py3-none-any.whl
  • Upload date:
  • Size: 56.4 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.8.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0c8e50204297883fa576f014cf6ba133d1cd3a34ff6748fd366d261e67a1e0a4
MD5 27af44cf94648912a00bb4101a9abe5c
BLAKE2b-256 975757a1ebbf139db7da37036369f4d03a37ab8a6bf110ec10e80b35fd0d66d5

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