starlette icon indicating copy to clipboard operation
starlette copied to clipboard

Make request.state generic

Open Kludex opened this issue 3 months ago • 3 comments

Discussed in https://github.com/Kludex/starlette/discussions/2562

Originally posted by patrick91 April 3, 2024 Hi there!

I was playing around with request.state, and I was wondering if we could make it generic, so that users can get better autocomplete on it (and maybe a bit of type safety too).

The way this would work is where you accept a Request you could also do Request[MyStateType], and request.state would be of type MyStateType

I think we can implement this in a backwards compatible way if we use TypeVar from typing extensions with a default value of any, like this:

from typing import Any
from typing_extensions import TypeVar

RequestState = TypeVar("RequestState", default=Any)  # this could also be `State`

This means that any current code wouldn't throw an error, and power users could type the state var 😊

The only issue I see at the moment is that State is a wrapper on a dict, but doesn't provide getitem, and my assumption is that types for State would be TypedDicts 😊 but is should be easy to add support for that. But I'm also saying this without knowing the reason why the state is a class on top of a dict, so I might be wrong :D

Kludex avatar Sep 11 '25 06:09 Kludex

I've tried on https://github.com/Kludex/starlette/pull/2944, but it would be a breaking change.

I've also opened https://github.com/python/typing/discussions/1457 to try to get some advice regarding TypedDict -> dataclass behavior transformation, but that's not possible yet, nor there seems much interest.


The way to move forward seems to allow State to become also a dictionary-like object, and then make Request[State], and on as an end goal, make the application be generic over state, but without the need to type it, so something like this:

from contextlib import asynccontextmanager
from typing import TypedDict

from starlette.applications import Starlette
from starlette.routing import Route


class MyState(TypedDict):
    count: int


@asynccontextmanager
async def lifespan(app: Starlette) -> AsyncGenerator[MyState]:
    yield {"count": 1}

async def homepage(request: Request[MyState]) -> Response:
    print(request.state["count"])
    print(request.state.count)  # this needs to work for backwards compatibility
    return Response(status_code=204)

app = Starlette(lifespan=lifespan, routes=[Route(homepage, "/")])

reveal_type(app)  # Starlette[MyState]

Or maybe we can create an alternative API for lifespan which would make the path cleaner to support any kind of object in the lifespan state...

Kludex avatar Sep 11 '25 07:09 Kludex

I've implemented a possible solution in #3036.

I think implementing "make the application be generic over state" is a bit more complicated, since the routes parameter is a Sequence[BaseRoute], and not Sequence[Route].

Kludex avatar Oct 09 '25 08:10 Kludex

Would it be feasible to support a small mypy plugin that lets state.count be type-checked the same as state["count"] when using a TypedDict schema? I’m not suggesting this must be included; apologies in advance if this is out of scope.

PerumallaGiridhar avatar Oct 30 '25 06:10 PerumallaGiridhar