· 5 min read ·

Starlette Reaches 1.0: What the Version Number Means for Python's Async Foundation

Source: simonwillison

Python’s async web ecosystem has a quiet load-bearing wall most people have never directly touched: Starlette. If you have used FastAPI, you have used Starlette. If you have written ASGI middleware, you have probably leaned on Starlette’s primitives. And now, after years of 0.x releases that were production-grade in practice but not in contract, Starlette has reached 1.0.

The version number deserves more attention than a typical point release.

What Starlette Actually Is

Starlette is not a full-stack framework. Tom Christie, who also created Django REST Framework, built it as a toolkit: a coherent collection of ASGI primitives that you can use individually or compose into a full application. The routing, middleware, request/response, WebSocket handling, background tasks, and static file serving are all separate pieces that happen to fit together well.

The minimal case looks like this:

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

async def homepage(request):
    return JSONResponse({'status': 'ok'})

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

Run that with Uvicorn and you have a working ASGI app in under ten lines. But the more interesting parts are the building blocks underneath: Request, Response, WebSocket, BackgroundTask, Middleware, Router, Mount. These are the things FastAPI, Litestar, and a dozen other frameworks either use directly or copy the design of.

Starlette also ships a TestClient built on HTTPX that can drive an ASGI app synchronously in tests, which sounds minor but is genuinely useful:

from starlette.testclient import TestClient

client = TestClient(app)
response = client.get('/')
assert response.status_code == 200

This client handles the ASGI lifecycle correctly, starts and shuts down lifespan events, and works without running an actual server. Libraries that wrote their own test infrastructure before Starlette’s TestClient matured generally regret it.

The Long 0.x Period

Starlette’s first release was in 2018. For roughly eight years, it shipped under 0.x semantics, which technically permits breaking changes in any minor release. In practice, the library was stable enough to be the foundation of FastAPI, which became one of the most widely deployed Python web frameworks in production.

The gap between “stable enough for production” and “1.0” is real, though. Under semantic versioning, a 0.x library is making no stability promise. Maintainers can and do break APIs between minor versions, requiring dependents to pin carefully. FastAPI’s pyproject.toml historically specified tight Starlette version ranges precisely because the API surface could shift. Any library author building on top of Starlette faced the same problem: you had to track Starlette releases closely or risk breakage.

The pre-1.0 period did produce some genuine churn. The middleware interface changed as the ASGI spec itself matured. Pure ASGI middleware replaced the older class-based approach:

# Older style, still common in documentation from 2021-2022
class MyMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        response = await call_next(request)
        return response

# Pure ASGI middleware, lower overhead, more explicit
class MyMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope['type'] == 'http':
            # intercept here
            pass
        await self.app(scope, receive, send)

The BaseHTTPMiddleware approach is simpler to write but carries overhead because it requires two tasks and a queue for every request. The pure ASGI form is more boilerplate but cheaper. Starlette kept BaseHTTPMiddleware for ergonomics, documented its trade-offs, and let users choose. That kind of accumulated design surface, stable but carrying historical decisions, is typical of a library ready for 1.0.

What 1.0 Commits To

A 1.0 release under semantic versioning means public API changes require a major version bump. For an infrastructure library like Starlette, this is meaningful: downstream projects can now declare starlette>=1.0,<2.0 and trust that minor releases will not break them.

For FastAPI, this matters significantly. Sebastián Ramírez has kept FastAPI tightly coupled to Starlette’s internals, using non-public APIs in places that occasionally caused compatibility problems when Starlette changed something. With a stable 1.0 contract, both projects can move with more confidence. FastAPI can specify a broader compatible range; Starlette maintainers get clearer signals about which APIs are depended on externally.

The 1.0 release also typically comes with cleanup: deprecated APIs removed, inconsistencies resolved, Python version support narrowed to whatever the current maintenance window covers. That kind of housekeeping is harder to do under 0.x semantics because the informal stability norm many projects have adopted still creates friction around breaking changes.

The ASGI Ecosystem Context

Starlette has shaped Python’s async web programming model more than most people acknowledge. The ASGI specification defines the interface between async Python web servers and applications, but a spec without a reference implementation is just a document. Starlette made ASGI usable early on, before there were many production ASGI servers. Uvicorn, Hypercorn, and Daphne all implement the ASGI spec; Starlette’s approach to consuming that spec set patterns that most frameworks followed.

The request/response model Starlette uses maps directly to the ASGI connection scope:

async def app(scope, receive, send):
    assert scope['type'] == 'http'
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [[b'content-type', b'text/plain']],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, World!',
    })

Starlette wraps this low-level interface into Request and Response objects while keeping the underlying model transparent. The lifespan protocol, which handles startup and shutdown events, got standardized partly because Starlette implemented it early and its behavior became the de facto reference.

Django’s async story, by contrast, moved much more slowly. Django added async views in 3.1 (2020), but the ORM remained synchronous until asgiref’s sync_to_async bridging became the accepted pattern. Django is still working through what it means to be an async framework rather than a sync framework with async support bolted on. Starlette was async-native from day one, which meant it could design around async/await rather than retrofitting it.

aiohttp predates Starlette and takes a different approach: it bundles an HTTP client and server together, has its own middleware model, and does not implement ASGI. For many use cases aiohttp works well, but its non-ASGI design means you cannot swap in Uvicorn or use standard ASGI middleware. That portability matters when you want to run the same application under different servers or compose middleware from different projects.

Why the Foundation Matters

Most developers working with FastAPI have never written a line of Starlette directly. They write path operation functions, use FastAPI’s dependency injection, and deploy behind Uvicorn. Starlette is the layer they do not think about, which is exactly the kind of software that most benefits from stability guarantees.

When a foundational library stays at 0.x for years, it creates a strange split: the library is treated as stable by its users but lacks the formal commitment that would let the ecosystem relax its dependency management. Maintainers of libraries that wrap or extend Starlette have been doing extra work to track compatibility across minor versions. That work goes away with a 1.0 contract.

For anyone building on the Python async web stack today, Starlette 1.0 is not a headline feature release. It is a signal that the foundation is declared finished in the sense that matters: the public interface is known, committed to, and will only change at major version boundaries. For infrastructure code, that is the most valuable thing a maintainer can offer.

Was this interesting?