Make request.state generic
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
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...
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].
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.