Simon Willison noted the release recently, and it’s worth pausing on: Starlette, the ASGI toolkit that has quietly underpinned a large chunk of Python’s async web story, has reached 1.0. For a library that has been production-ready and widely deployed for years, the milestone is less about technical maturity and more about a formal commitment to stability.
What Starlette Actually Is
Most Python developers who have touched async web work know Starlette indirectly, through FastAPI. Sebastián Ramírez built FastAPI on top of Starlette’s routing, request/response primitives, middleware system, and WebSocket support. When you write a FastAPI application, you’re using Starlette for most of the HTTP machinery. The dependency is not incidental; it’s structural.
But Starlette is its own complete thing. Tom Christie, who also built Django REST Framework and HTTPX, designed Starlette as a toolkit rather than a framework. You can use it at whatever level of abstraction makes sense. A raw ASGI callable, a Router with explicit route registration, or a full Starlette application object with middleware stacks and exception handlers — all are valid entry points.
Here’s a minimal Starlette application that gives you a sense of the surface area:
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route
async def homepage(request: Request) -> JSONResponse:
return JSONResponse({"status": "ok"})
app = Starlette(routes=[
Route("/", homepage),
])
That’s it. No decorators on the function. No global app object the route has to reference. The route table is explicit data, which makes it composable and testable in ways that decorator-based routing often isn’t.
Starlette also ships WebSocket support, background tasks, server-sent events, static file serving, Jinja2 templating, session middleware, and a test client built on HTTPX. It’s not minimal in the sense of being sparse; it’s minimal in the sense of not making decisions you didn’t ask it to make.
The Long Road Through 0.x
Starlette has been at 0.x versions since its initial release in 2018. That’s nearly eight years of production deployments, major framework adoption, and ecosystem growth — all technically under a version number that signals “not yet stable by semver convention.”
This is a common pattern in Python library development. Projects stay at 0.x either because maintainers want to preserve the ability to break APIs without ceremony, or because the milestone of 1.0 carries psychological weight they’re not ready to commit to, or simply because the version number drifts out of sync with the actual maturity of the software.
For Starlette specifically, the 0.x period involved real API evolution. The routing system was reworked. Middleware handling changed. anyio integration replaced direct asyncio usage, which made Starlette compatible with Trio as well. These were meaningful changes, and staying at 0.x gave the project room to make them.
But there’s a cost to that indefinite 0.x state, especially for downstream consumers. If you’re maintaining a library built on Starlette, or writing documentation for a framework that depends on it, “Starlette 0.x” means any minor version bump could theoretically break your assumptions. The practical reality is that Starlette’s maintainers have been careful, but the version number doesn’t reflect that care.
What 1.0 Commits To
Under semantic versioning, a 1.0.0 release is a commitment: breaking changes require a major version bump. That’s a different contract than 0.x, where anything goes in theory.
For a library in Starlette’s position, this commitment ripples downstream. FastAPI pins or ranges Starlette versions in its dependencies. Third-party middleware packages, authentication libraries, and testing utilities all have to manage their Starlette compatibility. When Starlette was at 0.x, every release was potentially a compatibility event. At 1.x, the signal is clearer: if you’re on 1.x and a new 1.y comes out, your code should still work.
This matters practically for library authors. If you’re writing a Starlette middleware package, you can now declare starlette>=1.0,<2.0 in your dependencies with some confidence about what that range actually covers. That was harder to express cleanly with 0.x versions.
For FastAPI, the effect is probably modest in the short term since Sebastián has always tracked Starlette closely. But over time, having a stable 1.x series to target should reduce the friction of keeping the ecosystem in sync.
The ASGI Ecosystem Context
Starlette didn’t emerge in a vacuum. The ASGI specification itself, which defines how Python async web servers and applications communicate, was formalized around the same time Starlette launched. Django Channels was the main prior art, but it was heavy and Django-specific.
Starlette filled a gap: a lightweight, framework-agnostic way to build ASGI applications and compose ASGI middleware. Uvicorn, also by Tom Christie, became the standard ASGI server. The combination gave Python a credible async web story that could compete with Node’s ecosystem without requiring developers to abandon the Python standard library.
Other ASGI frameworks have emerged in the years since. Litestar (formerly Starlite) is a notable one, offering more opinions and more built-in features than Starlette while targeting a similar audience. Blacksheep focuses on performance. Quart bridges Flask’s API onto ASGI. Each of these makes different trade-offs.
But Starlette’s position in the ecosystem is unusual because it’s foundational rather than competitive. FastAPI’s success has made Starlette’s continuation important to a large number of projects that don’t even know they depend on it. The 1.0 release, in this light, is partly a signal of long-term maintenance commitment, not just an API stability declaration.
The Toolkit Philosophy Holds Up
One thing worth appreciating about Starlette eight years in is that the toolkit philosophy has aged well. Compare it to Flask, which is a fine framework but one where swapping out pieces is harder because the opinions are baked in more deeply. Starlette’s components are genuinely composable.
You can use just the Request and Response classes. You can use the Router without the full Starlette application object. You can write ASGI middleware that works with any ASGI app, not just Starlette. The test client is built on HTTPX, so it supports the same interface as production HTTP requests:
from starlette.testclient import TestClient
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
This composability is what made Starlette a good foundation for FastAPI. Ramírez could add Pydantic validation, dependency injection, and automatic OpenAPI generation as layers on top without forking or patching Starlette’s internals. That’s a design property worth naming: good libraries are ones that enable other libraries rather than requiring them.
Reaching 1.0 Is Still Worth Marking
There’s a tendency to be dismissive of version number milestones for mature software. “It was already stable” is often true. But version numbers are communication, and 1.0 communicates something 0.37 doesn’t.
For developers evaluating libraries, 0.x suggests work in progress. For organizations with procurement processes, 0.x can trigger additional scrutiny. For downstream maintainers, 0.x means ambiguous compatibility windows. Starlette clearing all of that after years of proven use is a straightforward improvement to the library’s practical standing.
Tom Christie and the Starlette maintainers have been building Python web infrastructure for a long time. Django REST Framework remains one of the most widely used Python libraries in production. HTTPX has largely displaced Requests for async HTTP work. Uvicorn ships in every serious Python async deployment. Starlette 1.0 is consistent with that track record: careful, deliberate, and a bit overdue in the best possible way.