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.
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
🧪 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
979d60176c41585702b5cc1081dffb8c07b9f97ca43f9443a675857b68b37017
|
|
| MD5 |
2e328a30886c0fa7fb7f0a163f96d959
|
|
| BLAKE2b-256 |
abe7ca941adf6bb1cdcaf2e9510c6e57c9a2495ac93c4adc978e19d2e62e4225
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c8e50204297883fa576f014cf6ba133d1cd3a34ff6748fd366d261e67a1e0a4
|
|
| MD5 |
27af44cf94648912a00bb4101a9abe5c
|
|
| BLAKE2b-256 |
975757a1ebbf139db7da37036369f4d03a37ab8a6bf110ec10e80b35fd0d66d5
|