Framework Integrations¶
While Picodi is a general-purpose dependency injection library usable in any Python application, it also provides specific integrations to work smoothly with popular web frameworks like Starlette and FastAPI.
General Approach¶
The core principles of Picodi (@inject, Provide, scopes, overrides) work the same regardless of the framework.
You typically inject dependencies into your framework’s entry points, such as:
Route handlers (views) in web frameworks.
Command handlers in CLI applications.
Task functions in background workers.
Picodi’s specific integrations often provide helpers like custom scopes (e.g., request scope) or middleware to manage dependency lifecycles within the framework’s context.
Starlette Integration¶
Picodi provides helpers for Starlette applications, primarily for managing request-scoped dependencies.
RequestScope¶
Class:
RequestScopeInherits from:
ContextVarScopeBehavior: Creates and caches dependency instances within the context of a single HTTP request. Each request gets its own set of instances for dependencies using this scope.
Cleanup: Requires manual shutdown, typically handled by the
RequestScopeMiddleware.
RequestScopeMiddleware¶
Class:
RequestScopeMiddlewarePurpose: An ASGI middleware that automatically handles the lifecycle of
RequestScopedependencies.It can optionally initialize specified dependencies at the start of a request using
registry.init().It automatically calls
registry.shutdown(scope_class=RequestScope)at the end of the request to clean up any request-scoped yield dependencies.
Usage:
Define your request-scoped dependency using
@registry.set_scope(RequestScope).Add the
RequestScopeMiddlewareto your Starlette application.
Warning
The RequestScopeMiddleware must ve added first in the middleware stack,
before any other middlewares.
# dependencies.py
from picodi import registry
from picodi.integrations.starlette import RequestScope
import uuid
@registry.set_scope(RequestScope)
def get_request_id():
req_id = str(uuid.uuid4())
print(f"REQUEST SCOPE: Generated request ID: {req_id}")
yield req_id # Use yield if cleanup needed, otherwise return
print(f"REQUEST SCOPE: Cleaning up request ID: {req_id}")
# app.py
from picodi import Provide, inject
from picodi.integrations.starlette import RequestScopeMiddleware
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
# from dependencies import get_request_id
@inject
def service(request_id: str = Provide(get_request_id)):
return request_id
@inject
async def homepage(_: Request, request_id: str = Provide(get_request_id)):
# The request_id will be unique per request
request_id_from_service = service()
return JSONResponse(
{
"request_id": request_id,
"request_id_from_service": request_id_from_service,
}
)
routes = [
Route("/", homepage),
]
# Add the middleware
middleware = [
Middleware(RequestScopeMiddleware)
# You can optionally pass dependencies_for_init to the middleware
# Middleware(RequestScopeMiddleware, dependencies_for_init=[dep1, dep2])
]
app = Starlette(routes=routes, middleware=middleware)
# Run with: uvicorn app:app
# Accessing '/' will show the same request_id from the service and the view
# this is because `get_request_id` has `RequestScope` scope
FastAPI Integration¶
FastAPI has its own powerful dependency injection system, primarily focused on route parameters, validation, and request data parsing. Picodi can complement FastAPI’s system, especially for managing application-level services, shared resources, and complex dependency lifecycles that extend beyond a single request or need to be used outside of route handlers.
Why Use Picodi with FastAPI?¶
Scopes: Manage dependency lifecycles beyond FastAPI’s default (which is similar to Picodi’s
NullScope). UseSingletonScopefor shared clients,ContextVarScope/RequestScopefor request-level caching.Consistency: Use the same DI mechanism for dependencies shared between FastAPI routes, background tasks, CLI commands, etc.
Testability: Leverage Picodi’s overriding capabilities for application-level services.
Using Picodi Dependencies in FastAPI Routes¶
Picodi provides a special Provide() marker designed for FastAPI.
Method 1: Using @inject (Less Common in Routes)
You can use Picodi’s standard @inject on your route function, but you still need to wrap the
Provide marker with FastAPI’s Depends.
from fastapi import FastAPI, Depends
from picodi import inject
from picodi.integrations.fastapi import Provide # Use the fastapi version
app = FastAPI()
# Assume get_my_service is a Picodi dependency (sync or async)
def get_my_service():
print("Providing my_service")
return "My Service Instance"
@app.get("/inject-route")
@inject # Picodi's inject
async def route_with_inject(
# Need Depends() around Picodi's Provide()
service_instance: str = Depends(Provide(get_my_service)),
):
return {"service": service_instance}
Method 2: Using Provide(…, wrap=True) (Recommended for Routes)
To avoid the verbosity of Depends(Provide(...)) and the need for @inject on the route itself,
use the wrap=True argument with picodi.integrations.fastapi.Provide.
This tells Picodi to wrap the dependency in a way that FastAPI’s own DI system understands directly.
from fastapi import FastAPI
from picodi.integrations.fastapi import Provide # Use the fastapi version
app = FastAPI()
# Assume get_my_service is defined as before
def get_my_service():
print("Providing my_service")
return "My Service Instance"
@app.get("/wrapped-route")
async def route_without_inject(
# No @inject needed on the route!
# Provide(..., wrap=True) integrates with FastAPI's DI
service_instance: str = Provide(get_my_service, wrap=True)
):
return {"service": service_instance}
This is the preferred way to inject Picodi-managed dependencies into FastAPI route functions, as it leverages FastAPI’s DI for the route parameters while using Picodi for managing the dependency itself.
Combining FastAPI Depends and Picodi Provide¶
You can easily combine FastAPI’s dependencies (for things like path parameters, query parameters, security) with Picodi dependencies within the same function signature.
from fastapi import FastAPI, Depends, Path, HTTPException
from picodi.integrations.fastapi import Provide
from typing import Annotated
app = FastAPI()
# --- Picodi Dependency ---
class DatabaseClient:
def get_item(self, item_id: int):
print(f"DB Client: Fetching item {item_id}")
if item_id == 42:
return {"id": item_id, "name": "Widget"}
return None
def get_db_client():
return DatabaseClient()
# --- FastAPI Security Dependency ---
def get_current_user(token: str | None = None): # Example security dep
if token == "secret":
return {"username": "alice"}
raise HTTPException(status_code=401, detail="Invalid token")
# --- Route Combining Both ---
@app.get("/items/{item_id}")
async def get_item(
# FastAPI path parameter
item_id: Annotated[int, Path(title="The ID of the item to get")],
# FastAPI security dependency
current_user: Annotated[dict, Depends(get_current_user)],
# Picodi dependency using ``wrap=True``
db: DatabaseClient = Provide(get_db_client, wrap=True),
):
print(f"User {current_user['username']} requesting item {item_id}")
item = db.get_item(item_id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return item
Request-Scoped Dependencies in FastAPI¶
You can use the same RequestScopeMiddleware and
RequestScope
in FastAPI as you would in Starlette to manage request-scoped dependencies.
Warning
The RequestScopeMiddleware must ve added first in the middleware stack,
before any other middlewares.
import uuid
from fastapi import FastAPI
from picodi import registry
from picodi.integrations.fastapi import Provide, RequestScope, RequestScopeMiddleware
from starlette.middleware import Middleware
# Define request-scoped dependency
@registry.set_scope(RequestScope)
def get_request_correlation_id():
req_id = str(uuid.uuid4())[:8]
print(f"FastAPI Request Scope: Generated ID: {req_id}")
yield req_id
print(f"FastAPI Request Scope: Cleaning up ID: {req_id}")
# Add middleware to FastAPI app
app = FastAPI(middleware=[Middleware(RequestScopeMiddleware)])
@app.get("/request-id")
async def get_id(correlation_id: str = Provide(get_request_correlation_id, wrap=True)):
return {"correlation_id": correlation_id}
FastAPI Example Project¶
For a more comprehensive example of using Picodi with FastAPI, including different scopes and testing setups, see the example project:
Key Takeaways¶
Picodi integrates with Starlette and FastAPI, primarily via middleware and specialized
Providemarkers.Use
RequestScopeMiddlewareandRequestScopefor request-scoped dependencies in Starlette/FastAPI.In FastAPI, use
picodi.integrations.fastapi.Provide(..., wrap=True)to inject Picodi dependencies into routes without needing@injecton the route function.Combine FastAPI’s
Dependswith Picodi’sProvidefor flexible dependency management in routes.
Next, let’s review some Best Practices for using Picodi effectively.