Lifespan
Starlette applications can register a lifespan handler for dealing withcode that needs to run before the application starts up, or when the applicationis shutting down.
importcontextlibfromstarlette.applicationsimportStarlette@contextlib.asynccontextmanagerasyncdeflifespan(app):asyncwithsome_async_resource():print("Run at startup!")yieldprint("Run on shutdown!")routes=[...]app=Starlette(routes=routes,lifespan=lifespan)Starlette will not start serving any incoming requests until the lifespan has been run.
The lifespan teardown will run once all connections have been closed, andany in-process background tasks have completed.
Consider usinganyio.create_task_group()for managing asynchronous tasks.
Lifespan State
The lifespan has the concept ofstate, which is a dictionary thatcan be used to share the objects between the lifespan, and the requests.
importcontextlibfromtypingimportAsyncIterator,TypedDictimporthttpxfromstarlette.applicationsimportStarlettefromstarlette.requestsimportRequestfromstarlette.responsesimportPlainTextResponsefromstarlette.routingimportRouteclassState(TypedDict):http_client:httpx.AsyncClient@contextlib.asynccontextmanagerasyncdeflifespan(app:Starlette)->AsyncIterator[State]:asyncwithhttpx.AsyncClient()asclient:yield{"http_client":client}asyncdefhomepage(request:Request)->PlainTextResponse:client=request.state.http_clientresponse=awaitclient.get("https://www.example.com")returnPlainTextResponse(response.text)app=Starlette(lifespan=lifespan,routes=[Route("/",homepage)])Thestate received on the requests is ashallow copy of the state received on thelifespan handler.
Accessing State
The state can be accessed using either attribute-style or dictionary-style syntax.
The dictionary-style syntax was introduced in Starlette 0.52.0 (January 2026), with the idea ofimproving type safety when using the lifespan state, given thatRequest became a generic overthe state type.
fromcollections.abcimportAsyncIteratorfromcontextlibimportasynccontextmanagerfromtypingimportTypedDictimporthttpxfromstarlette.applicationsimportStarlettefromstarlette.requestsimportRequestfromstarlette.responsesimportPlainTextResponsefromstarlette.routingimportRouteclassState(TypedDict):http_client:httpx.AsyncClient@asynccontextmanagerasyncdeflifespan(app:Starlette)->AsyncIterator[State]:asyncwithhttpx.AsyncClient()asclient:yield{"http_client":client}asyncdefhomepage(request:Request[State])->PlainTextResponse:client=request.state["http_client"]reveal_type(client)# Revealed type is 'httpx.AsyncClient'response=awaitclient.get("https://www.example.com")returnPlainTextResponse(response.text)app=Starlette(lifespan=lifespan,routes=[Route("/",homepage)])Note
There were many attempts to make this work with attribute-style access instead ofdictionary-style access, but none were satisfactory, given they would have beenbreaking changes, or there were typing limitations.
For more details, see:
Running lifespan in tests
You should useTestClient as a context manager, to ensure that the lifespan is called.
fromexampleimportappfromstarlette.testclientimportTestClientdeftest_homepage():withTestClient(app)asclient:# Application's lifespan is called on entering the block.response=client.get("/")assertresponse.status_code==200# And the lifespan's teardown is run when exiting the block.