Migrating to v7

Responder 7.0 focuses on explicit runtime contracts: framework errors use problem details by default, auth can be declared at the app level, dependencies can be attached to routes as graph-aware guards, and optional production server tooling lives behind the server extra.

Problem Details by Default

Framework-generated errors now use RFC 9457-style application/problem+json responses by default:

{
    "type": "about:blank",
    "title": "Not Found",
    "status": 404,
    "detail": "Not Found"
}

This applies to framework errors such as 404, 405, request parsing failures, validation failures, request timeouts, auth failures, and production response-model validation failures. When validation details exist, the framework still includes them as the extension member errors:

{
    "type": "about:blank",
    "title": "Validation Error",
    "status": 422,
    "detail": "Validation failed",
    "errors": [{...}],
}

If you need the previous content-negotiated behavior while migrating, pass problem_details=False:

api = responder.API(problem_details=False)

App-Level Auth

Routes can still declare auth directly:

@api.get("/me", auth=bearer)
def me(req, resp, *, user):
    resp.media = {"user": user}

In v7, apps can also define a default auth scheme. Routes inherit it unless they explicitly opt out with auth=None:

api = responder.API(auth=bearer)

@api.post("/login", auth=None)
def login(req, resp):
    resp.media = {"token": issue_token()}

@api.get("/me")
def me(req, resp, *, user):
    resp.media = {"user": user}

When OpenAPI is enabled, inherited auth is documented on protected operations and auth=None marks public operations with an empty security requirement.

Auth helpers can also be wrapped with lightweight scope or role checks:

admin = bearer.requires("items:write")

@api.post("/items", auth=admin)
def create_item(req, resp, *, user):
    resp.media = {"user": user}

Responder reads scopes from a principal’s scopes or roles attribute/key. Missing scopes produce 403 and scoped OpenAPI security requirements are documented on the operation.

Route Dependency Guards

Depends(...) still injects local dependency values into handler parameters. Use route-level dependencies=[Depends(...)] when the dependency is a guard or setup step and the handler does not need its return value. This keeps the call in the dependency graph with caching/teardown behavior, and still works for side effects:

def require_user(req):
    if "Authorization" not in req.headers:
        responder.abort(401, detail="Not authenticated")

@api.get("/private", dependencies=[Depends(require_user)])
def private(req, resp):
    resp.media = {"ok": True}

Route dependencies follow the same lifecycle rules as parameter dependencies, including sync/async providers, sub-dependencies, and generator teardown.

For raw before/after hooks, use before=/after= to keep intent explicit. Hooks are not dependency providers: they run around the handler for request/response mutation or short-circuiting, while Depends(...) remains the path for dependency caching, sub-dependencies, and generator teardown.

The three route-local mechanisms are distinct:

  • Handler parameters with Depends(...) resolve a value and inject it into the handler.

  • dependencies=[Depends(...)] resolves graph-aware providers for side effects and ignores their return value.

  • before=/after= runs raw hooks with no dependency graph participation.

Route execution order is now explicit: global before_request hooks, route before hooks, auth, validation, route dependencies=..., handler, response-model checks, and finally after hooks.

This ordering matters if a route has both hooks and dependency-guards.

Request Model Removal

request_model= and req.state.validated have been removed. Use a required Pydantic-typed handler parameter for request-body validation instead:

class ItemIn(BaseModel):
    name: str

@api.post("/items")
async def create_item(req, resp, *, item: ItemIn):
    resp.media = item.model_dump()

This is the same validation path used by OpenAPI generation and returns 422 before the handler runs when the body is missing fields, has wrong types, or is not a JSON object.

Server Extra

The default install still includes the uvicorn runner used by api.run(). Install responder[server] when you want the optional Granian server too:

uv pip install 'responder[server]'

Then run the current app with Granian’s embedded ASGI server:

api.run(server="granian")

If Granian is not installed, server="granian" raises a runtime error with the install command. For multi-worker production deployments, point Granian’s CLI at your ASGI app:

granian --interface asgi --host 0.0.0.0 --port 8000 api:api

Request Method Type

req.method now returns an exact str. It is still uppercase ("GET", "POST", etc.) and comparisons remain case-sensitive. The old exported HTTPMethod subclass has been removed.

Python Support

Responder 7.0 requires Python 3.11 or newer; Python 3.10 is no longer supported. CPython 3.11–3.15 (including free-threaded builds) and PyPy 3.11 are tested in CI.