Wireup
Performant, concise and type-safe Dependency Injection for Python 3.8+
What is Dependency Injection?
Dependency Injection (DI) is a design pattern where dependencies are provided externally rather than created within objects. Wireup automates DI using Python's type system, with support for async, generators and other modern Python features.
Zero Runtime Overhead
New: Inject Dependencies in FastAPI with zero runtime overhead using Class-Based Handlers.
Features¶
โจ Simple & Type-Safe DI¶
Inject services and configuration using a clean and intuitive syntax without boilerplate.
@service
class Database:
pass
@service
class UserService:
def __init__(self, db: Database) -> None:
self.db = db
container = wireup.create_sync_container(services=[Database, UserService])
user_service = container.get(UserService) # โ
Dependencies resolved.
# Clean domain objects: No annotations
class Database:
pass
class UserService:
def __init__(self, db: Database) -> None:
self.db = db
# Register services via factories
@service
def database_factory() -> Database:
return Database()
@service
def user_service_factory(db: Database) -> UserService:
return UserService(db)
container = wireup.create_sync_container(
services=[database_factory, user_service_factory]
)
user_service = container.get(UserService) # โ
Dependencies resolved.
๐ฏ Function Injection¶
Inject dependencies directly into functions with a simple decorator.
@inject_from_container(container)
def process_users(service: Injected[UserService]):
# โ
UserService injected.
pass
๐ Interfaces & Abstract Classes¶
Define abstract types and have the container automatically inject the implementation.
@abstract
class Notifier(abc.ABC):
pass
@service
class SlackNotifier(Notifier):
pass
notifier = container.get(Notifier) # โ
SlackNotifier instance.
๐ Managed Service Lifetimes¶
Declare dependencies as singletons, scoped, or transient to control whether to inject a fresh copy or reuse existing instances.
One instance per application. @service(lifetime="singleton")
is the default.
@service
class Database:
pass
One instance per scope/request, shared within that scope/request.
@service(lifetime="scoped")
class RequestContext:
def __init__(self) -> None:
self.request_id = uuid4()
When full isolation and clean state is required. Every request to create transient services results in a new instance.
@service(lifetime="transient")
class OrderProcessor:
pass
๐ญ Flexible Creation Patterns¶
Defer instantiation to specialized factories when complex initialization or cleanup is required. Full support for async and generators. Wireup handles cleanup at the correct time depending on the service lifetime.
class WeatherClient:
def __init__(self, client: requests.Session) -> None:
self.client = client
@service
def weather_client_factory() -> Iterator[WeatherClient]:
with requests.Session() as session:
yield WeatherClient(client=session)
class WeatherClient:
def __init__(self, client: aiohttp.ClientSession) -> None:
self.client = client
@service
async def weather_client_factory() -> AsyncIterator[WeatherClient]:
async with aiohttp.ClientSession() as session:
yield WeatherClient(client=session)
๐ก๏ธ Improved Safety¶
Wireup is mypy strict compliant and will not introduce type errors. It will also warn at the earliest possible stage about configuration errors to avoid surprises at runtime.
The container will raise errors at creation time about missing dependencies or other issues.
@service
class Foo:
def __init__(self, unknown: NotManagedByWireup) -> None:
pass
container = wireup.create_sync_container(services=[Foo])
# โ Parameter 'unknown' of 'Foo' depends on an unknown service 'NotManagedByWireup'.
Injected functions will raise errors at module import time rather than when called.
@inject_from_container(container)
def my_function(oops: Injected[NotManagedByWireup]):
pass
# โ Parameter 'oops' of 'my_function' depends on an unknown service 'NotManagedByWireup'.
Wireup integrations assert that requested injections in the framework are valid.
@app.get("/")
def home(foo: Injected[NotManagedByWireup]):
pass
wireup.integration.flask.setup(container, app)
# โ Parameter 'foo' of 'home' depends on an unknown service 'NotManagedByWireup'.
Configuration parameters are also checked for validity.
class Database:
def __init__(self, url: Annotated[str, Inject(param="db_url")]) -> None:
self.db = db
# โ Parameter 'url' of Type 'Database' depends on an unknown Wireup parameter 'db_url'.
๐ Framework-Agnostic¶
Wireup provides its own Dependency Injection mechanism and is not tied to specific frameworks. Use it anywhere you like.
๐ Share Services Between Application and CLI¶
Share the service layer between web applications and their accompanying CLIs using Wireup.
๐ Native Integration with popular frameworks¶
Integrate with popular frameworks for a smoother developer experience. Integrations manage request scopes, injection in endpoints, and lifecycle of services.
app = FastAPI()
container = wireup.create_async_container(services=[UserService, Database])
@app.get("/")
def users_list(user_service: Injected[UserService]):
pass
wireup.integration.fastapi.setup(container, app)
๐งช Simplified Testing¶
Wireup does not patch services and lets them be tested in isolation.
If the container is needed in tests, it can create parts of services or perform dependency substitution.
with container.override.service(target=Database, new=in_memory_database):
# The /users endpoint depends on Database.
# During the lifetime of this context manager, requests to inject `Database`
# will result in `in_memory_database` being injected instead.
response = client.get("/users")
Next Steps¶
- Getting Started - Follow the Getting Started guide for a more in-depth tutorial.
- Services
- Parameters