What’s Changed & Migration
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against FastAPI 0.142.2 (released 2026-09-30), Pydantic 2.13 and Starlette 1.7, checked on
2026-10-02. FastAPI supports Python 3.10 through 3.14; every current-style Python example on this page was executed on
Python 3.13 with TestClient or plain execution, and every deprecated or removed form is shown only as the old way
and was not run as a recommendation. FastAPI is still a 0.x project, so almost every release can contain a visible
change, and a large part of the material on the web (tutorials, blog posts, and the reference book listed in
the section bibliography) was written before many of those releases. This
page is the map between that older material and today’s documentation: which release changed what, on which date, what
the old code looked like, what to write now, which page of this section covers it, and where the official release
notes say it.
How to Read the Tables
Each row has the FastAPI version that introduced the change, the release date, the old way you will meet in older code, the new way, a link to the page of this section that covers it, and a link to the official release notes. The version links use the anchor form of the release notes page, which is the version without dots followed by the release date. Where a change belongs to a library or to Python rather than to a numbered FastAPI release, the version column says so and the last column links the official page that documents it instead.
Two rules of thumb make migrations safe. Pin FastAPI to an exact version in pyproject.toml or in a lock file, as the
official versions page advises for a 0.x project, and read the release notes entries marked as breaking before every
upgrade. Run the test suite (it prints a deprecation warning from Starlette about httpx and httpx2 with the current TestClient; see Testing) after each upgrade: most of the changes
below fail loudly at import time or on the first request, not silently in production.
The Dated Baseline
| Component | Version | What changed to get here |
|---|---|---|
FastAPI |
0.142.2 (2026-09-30) |
Still 0.x. The latest entries are 0.142.0 (2026-09-29, native OpenTelemetry), 0.142.1 (fixes repeated wrapping of endpoints in included routers) and 0.142.2 (startup no longer fails if automatic OpenTelemetry configuration fails). |
Python |
3.10 to 3.14 |
3.8 was dropped in 0.125.0 (2025-12-17) and 3.9 in 0.129.0 (2026-02-12). Python 3.14 is supported since 0.118.3 (2025-10-10) and the free-threaded build 3.14t since 0.136.0 (2026-04-16). |
Pydantic |
2.13, version 2 only |
FastAPI requires Pydantic 2.9 or newer. Pydantic v1 support was removed in 0.126.0 and the |
Starlette |
1.7, 0.46 or newer accepted |
Starlette 1.x is supported since 0.133.0 (2026-02-24). |
Install command |
|
Plain |
Installing, Running and Deploying
| Version | Date | Old way | New way | Covered on | Release notes |
|---|---|---|---|---|---|
0.111.0 |
2024-05-03 |
|
|
||
0.112.0 |
2024-08-02 |
|
|
||
0.128.8, 0.129.2 |
2026-02-11, 2026-02-21 |
|
|
||
Not tied to a FastAPI release |
n/a |
|
|
||
0.93.0 |
2023-03-07 |
|
a |
||
0.116.0 |
2025-07-07 |
Hand-built deployment pipelines only |
|
||
0.125.0, 0.129.0 |
2025-12-17, 2026-02-12 |
Python 3.8 and 3.9 supported |
Python 3.10 or newer |
||
0.118.3, 0.136.0 |
2025-10-10, 2026-04-16 |
Python 3.13 as the newest tested interpreter |
Python 3.14, and the free-threaded 3.14t build |
||
0.140.0 |
2026-07-24 |
No agent guidance in the docs |
The FastAPI Agent Skill ("Library Skills") for coding agents |
||
Not tied to a FastAPI release |
n/a |
Editor setup left to the reader |
The official editor extension described in the docs |
The old running style still works, because Uvicorn is a normal ASGI server. What changed is the documented default.
Server options that the fastapi command does not expose are reached through Uvicorn directly, as the
Deployment page shows.
# Old way (still valid, but no longer the documented default)
uvicorn main:app --reload
# New way
uv add "fastapi[standard]"
fastapi dev # auto-reload, development
fastapi run --workers 4 # production
Source: FastAPI: FastAPI CLI
Startup and Shutdown: From on_event to lifespan
Older code registers startup and shutdown handlers with a decorator that is now deprecated:
from fastapi import FastAPI
app = FastAPI()
resources: dict[str, str] = {}
@app.on_event("startup")
async def startup() -> None:
resources["db"] = "connected"
@app.on_event("shutdown")
async def shutdown() -> None:
resources.clear()
The replacement is one async context manager: everything before yield is startup, everything after is shutdown, and
the two halves can share local variables. The with TestClient(app) block runs the lifespan; without the with, it
does not run.
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.testclient import TestClient
resources: dict[str, str] = {}
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
resources["db"] = "connected" # startup
yield
resources.clear() # shutdown
app = FastAPI(lifespan=lifespan)
@app.get("/status")
def status() -> dict[str, str]:
return resources
with TestClient(app) as client:
print(client.get("/status").json())
print("after shutdown:", resources)
{'db': 'connected'}
after shutdown: {}
Python Types and Pydantic v2
| Version | Date | Old way | New way | Covered on | Release notes |
|---|---|---|---|---|---|
0.95.0 |
2023-03-18 |
|
|
||
0.126.0 |
2025-12-20 |
Pydantic v1 models and a |
Pydantic v2 only; the v1 compatibility path is gone |
||
0.128.0 |
2025-12-27 |
|
|
||
0.128.2 |
2026-02-05 |
Type aliases written as plain assignments |
PEP 695 |
The Pydantic v1 to v2 Replacements
These are the replacements to apply to code written for Pydantic v1. They come from the Pydantic migration guide and from the FastAPI documentation, and they are shown in full on Pydantic v2 Essentials.
| Pydantic v1 (old way) | Pydantic v2 (new way) | Notes |
|---|---|---|
|
|
|
|
|
|
|
|
The same names apply to lists, strings and sets |
|
No default at all |
A field with no default is required |
|
|
Class-based |
|
|
Validators are classmethods in v2 |
|
Still required in v2: |
Add |
Error |
Error |
Error payloads also carry |
Models accepted positional arguments in some cases |
Keyword-only construction |
|
from pydantic import BaseModel, Field, validator
class Book(BaseModel):
title: str = Field(..., min_items=1)
tags: list[str] = Field(default_factory=list, min_items=0)
class Config:
orm_mode = True
@validator("title")
def strip_title(cls, value):
return value.strip()
payload = Book(title=" Dune ").dict()
Source: Pydantic: Migration Guide
The same model in Pydantic v2 style. The script prints the dump, the new error type for a missing field, and shows
that positional construction is rejected.
from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
class Book(BaseModel):
model_config = ConfigDict(from_attributes=True)
title: Annotated[str, Field(min_length=1, pattern=r"^\S.*$")]
tags: Annotated[list[str], Field(max_length=5)] = []
subtitle: str | None # no default: still required in v2
@field_validator("title")
@classmethod
def strip_title(cls, value: str) -> str:
return value.strip()
class Row:
title = "Dune"
tags: list[str] = []
subtitle = None
print(Book(title="Dune", subtitle=None).model_dump())
print(Book.model_validate(Row()).title)
try:
Book(title="Dune")
except ValidationError as exc:
print(exc.errors()[0]["type"], exc.errors()[0]["loc"])
try:
Book("Dune") # type: ignore[misc]
except TypeError as exc:
print("positional:", type(exc).__name__)
Source: Pydantic: Migration Guide
{'title': 'Dune', 'tags': [], 'subtitle': None}
Dune
missing ('subtitle',)
positional: TypeError
Annotated Instead of Default-Value Metadata
The older style is still accepted, so you will meet it in existing code. New code uses Annotated, because the
default value stays a real default (and works for plain Python calls) and the metadata can be shared through an alias.
from fastapi import Depends, FastAPI, Query
app = FastAPI()
def pagination(limit: int = Query(default=10, ge=1, le=100)) -> int:
return limit
@app.get("/books")
def list_books(q: str | None = Query(default=None, max_length=50), limit: int = Depends(pagination)):
return {"q": q, "limit": limit}
from typing import Annotated
from fastapi import Depends, FastAPI, Query
from fastapi.testclient import TestClient
app = FastAPI()
def pagination(limit: Annotated[int, Query(ge=1, le=100)] = 10) -> int:
return limit
Limit = Annotated[int, Depends(pagination)] # reusable alias
@app.get("/books")
def list_books(limit: Limit, q: Annotated[str | None, Query(max_length=50)] = None) -> dict:
return {"q": q, "limit": limit}
client = TestClient(app)
print(client.get("/books", params={"q": "dune", "limit": 5}).json())
print(client.get("/books", params={"limit": 500}).status_code)
{'q': 'dune', 'limit': 5}
422
from typing import Annotated
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
type PageSize = Annotated[int, Query(ge=1, le=100)]
app = FastAPI()
@app.get("/books")
def list_books(size: PageSize = 20) -> dict[str, int]:
return {"size": size}
client = TestClient(app)
print(client.get("/books", params={"size": 7}).json(), client.get("/books", params={"size": 0}).status_code)
Source: FastAPI: Python Types Intro
{'size': 7} 422
Security Libraries and Status Codes
| Version | Date | Old way | New way | Covered on | Release notes |
|---|---|---|---|---|---|
0.111.1 |
2024-07-14 |
|
|
||
0.118.0 |
2025-09-29 |
|
|
||
Python 3.12 |
n/a |
|
|
||
0.122.0 |
2025-11-24 |
Missing credentials answered with |
|
A related misunderstanding in older material is treating the password flow and the authorization code flow as one thing. They are different OAuth2 flows: the password flow sends credentials to your own token endpoint, while the authorization code flow redirects the user to an identity provider. The current form of the password flow is shown below.
import os
from datetime import datetime, timedelta
from jose import jwt
from passlib.context import CryptContext
SECRET_KEY = os.environ["SECRET_KEY"] # never hardcode a key
pwd = CryptContext(schemes=["bcrypt"])
token = jwt.encode({"sub": "ana", "exp": datetime.utcnow() + timedelta(minutes=30)}, SECRET_KEY)
The current example reads the signing key from the environment (generate one with openssl rand -hex 32 and keep it in
your secret store), uses PyJWT and pwdlib, and shows that a missing token now answers 401.
import os
import secrets
from datetime import datetime, timedelta, timezone
from typing import Annotated
import jwt
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from fastapi.testclient import TestClient
from pwdlib import PasswordHash
# In a real app this comes from settings (openssl rand -hex 32); a random value keeps the demo self-contained.
SECRET_KEY = os.environ.get("BOOKSHELF_SECRET_KEY") or secrets.token_hex(32)
ALGORITHM = "HS256"
password_hash = PasswordHash.recommended() # Argon2
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
def create_token(subject: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=30)
return jwt.encode({"sub": subject, "exp": expire}, SECRET_KEY, algorithm=ALGORITHM)
@app.get("/me")
def me(token: Annotated[str, Depends(oauth2_scheme)]) -> dict[str, str]:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return {"user": payload["sub"]}
hashed = password_hash.hash("a-test-password")
print(password_hash.verify("a-test-password", hashed))
client = TestClient(app)
print(client.get("/me").status_code)
print(client.get("/me", headers={"Authorization": f"Bearer {create_token('ana')}"}).json())
True
401
{'user': 'ana'}
Data Access, Settings and Dependencies
The older way to reach a database was a module-global connection, with environment variables choosing between a fake
and a real module. The documented way is a session dependency with yield over SQLModel (the SQL tutorial was rewritten
in 0.115.1), configuration through pydantic-settings, and test doubles through app.dependency_overrides.
| Version | Date | Old way | New way | Covered on | Release notes |
|---|---|---|---|---|---|
0.115.1 |
2024-10-12 |
Module-global |
SQLModel with a session dependency that uses |
||
Not tied to a FastAPI release |
n/a |
Reading |
|
||
Not tied to a FastAPI release |
n/a |
Swapping a module, or monkeypatching, to replace a dependency in tests |
|
||
0.106.0 |
2023-12-25 |
Exit code of a |
|
||
0.118.0 |
2025-09-29 |
Exit code ran before the response was sent (the behavior introduced by 0.106.0) |
Exit code runs after the response has been sent, as it did before 0.106.0 |
||
0.121.0 |
2025-11-03 |
The exit code of a |
|
||
0.123.5 |
2025-12-02 |
A |
|
from collections.abc import Iterator
from functools import cache, partial
from typing import Annotated
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="BOOKSHELF_")
database_url: str = "sqlite:///./bookshelf.db"
@cache
def get_settings() -> Settings:
return Settings()
events: list[str] = []
def get_session(settings: Annotated[Settings, Depends(get_settings)]) -> Iterator[str]:
events.append("open " + settings.database_url)
try:
yield "session"
finally:
events.append("close")
def greeting(prefix: str) -> str:
return prefix + "!"
app = FastAPI()
@app.get("/books")
def books(session: Annotated[str, Depends(get_session, scope="function")]) -> dict[str, str]:
return {"using": session}
@app.get("/hello")
def hello(text: Annotated[str, Depends(partial(greeting, "hello"))]) -> str:
return text
client = TestClient(app)
print(client.get("/books").json(), events)
print(client.get("/hello").json())
app.dependency_overrides[get_session] = lambda: "fake-session"
print(client.get("/books").json())
{'using': 'session'} ['open sqlite:///./bookshelf.db', 'close']
hello!
{'using': 'fake-session'}
Testing and Templates
Older material often tests by calling path functions directly, which skips validation, dependencies and serialization.
The documentation now tests through the application: TestClient, dependency_overrides, with TestClient(app) to run
the lifespan, WebSocket sessions, and async tests with pytest.mark.anyio and httpx.AsyncClient over
ASGITransport. The TestClient shown elsewhere on this page is the synchronous form; the async form is below. See
Testing for the full treatment.
import anyio
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root() -> dict[str, str]:
return {"message": "Tomato"}
async def main() -> None:
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/")
print(response.status_code, response.json())
anyio.run(main)
Source: FastAPI: Async Tests
200 {'message': 'Tomato'}
Template rendering changed its call signature. Documentation updated to the new Starlette arguments in 0.108.0 (2023-12-26): the request is now the first argument and is no longer put into the context dictionary.
return templates.TemplateResponse("books.html", {"request": request, "books": books})
Source: FastAPI: Templates
import tempfile
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.testclient import TestClient
directory = Path(tempfile.mkdtemp())
(directory / "books.html").write_text("<ul>{% for b in books %}<li>{{ b }}</li>{% endfor %}</ul>")
templates = Jinja2Templates(directory=directory)
app = FastAPI()
@app.get("/books", response_class=HTMLResponse)
def books(request: Request):
return templates.TemplateResponse(request, "books.html", {"books": ["Dune", "Emma"]})
print(TestClient(app).get("/books").text)
<ul><li>Dune</li><li>Emma</li></ul>
| Version | Date | Old way | New way | Covered on | Release notes |
|---|---|---|---|---|---|
Not tied to a FastAPI release |
n/a |
Calling path functions directly in tests, bypassing validation and dependencies |
|
||
0.108.0 |
2023-12-26 |
|
|
Forms and Parameters
| Version | Date | Old way | New way | Covered on | Release notes |
|---|---|---|---|---|---|
0.113.0 |
2024-09-05 |
One |
A Pydantic model for a form: |
||
0.115.0 |
2024-09-17 |
One |
A Pydantic model for a group of query, header or cookie parameters, with |
||
0.136.3 |
2026-05-23 |
Header names with underscores matched when |
Such headers are no longer accepted when |
A form is a POST body, never a GET: older tutorials sometimes declare Form() on a GET endpoint, which a browser form
cannot meaningfully use. The current form models:
from typing import Annotated, Literal
from fastapi import FastAPI, Form, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class BookForm(BaseModel):
model_config = {"extra": "forbid"}
title: str
year: int
class Filters(BaseModel):
model_config = {"extra": "forbid"}
limit: int = Field(100, gt=0, le=100)
order_by: Literal["title", "year"] = "title"
@app.post("/books")
def create(form: Annotated[BookForm, Form()]) -> BookForm:
return form
@app.get("/books")
def search(filters: Annotated[Filters, Query()]) -> Filters:
return filters
client = TestClient(app)
print(client.post("/books", data={"title": "Dune", "year": "1965"}).json())
print(client.get("/books", params={"order_by": "year"}).json())
print(client.get("/books", params={"unknown": "x"}).status_code)
Source: FastAPI: Query Parameter Models
{'title': 'Dune', 'year': 1965}
{'limit': 100, 'order_by': 'year'}
422
Form models are documented on Form Models; the example
above applies the same extra="forbid" rule to both.
Responses and OpenAPI
| Version | Date | Old way | New way | Covered on | Release notes |
|---|---|---|---|---|---|
0.89.0 |
2023-01-07 |
|
The return annotation declares the response model: |
||
0.117.0 |
2025-09-20 |
A |
|
||
0.130.0 |
2026-02-22 |
Serialization performed in Python before the response was rendered |
Pydantic (Rust) serializes JSON directly when a return type or response model exists |
Response Models & Return Types and Performance & Observability |
|
0.131.0 |
2026-02-22 |
|
A return type or response model; both classes are deprecated |
||
0.99.0 |
2023-06-30 |
OpenAPI 3.0.2 and JSON Schema draft 7 |
OpenAPI 3.1.0 and JSON Schema 2020-12, with webhooks and Swagger UI 5 |
||
0.102.0 |
2023-08-25 |
One schema for both request and response |
|
||
0.103.0 |
2023-08-26 |
|
|
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
app = FastAPI(default_response_class=ORJSONResponse)
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Book(BaseModel):
title: str
isbn: str | None = None
@app.get("/books/{book_id}")
def read_book(book_id: int) -> Book: # response model from the annotation
return Book(title="Dune")
@app.delete("/books/{book_id}", status_code=204)
def delete_book(book_id: int) -> None: # no body
return None
client = TestClient(app)
print(client.get("/books/1").json())
print(client.delete("/books/1").status_code, repr(client.delete("/books/1").content))
print(client.get("/openapi.json").json()["openapi"])
{'title': 'Dune', 'isbn': None}
204 b''
3.1.0
New Since Older Material
These entries have no old way to replace: older tutorials and the reference book do not mention them at all, or mention a third-party package that the framework now replaces.
| Version | Date | Old way | New way | Covered on | Release notes |
|---|---|---|---|---|---|
0.132.0 |
2026-02-23 |
JSON bodies parsed whatever the |
Strict |
||
0.133.0 |
2026-02-24 |
Starlette 0.x only |
Starlette 1.0 and newer supported |
||
0.134.0 |
2026-02-27 |
Hand-written streaming of one JSON document per line |
|
||
0.135.0 |
2026-03-01 |
A third-party package (such as |
|
||
0.137.0, 0.137.2 |
2026-06-14, 2026-06-18 |
|
|
||
0.138.0, 0.139.0, 0.141.0 |
2026-06-20, 2026-07-01, 2026-07-29 |
|
|
||
0.142.0 |
2026-09-29 |
|
Native OpenTelemetry support: traces, metrics and logs configured with |
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel
class Book(BaseModel):
title: str
strict = FastAPI()
lenient = FastAPI(strict_content_type=False) # opt out only for legacy clients
@strict.post("/books")
def create_strict(book: Book) -> Book:
return book
@lenient.post("/books")
def create_lenient(book: Book) -> Book:
return book
body = '{"title": "Dune"}'
print(TestClient(strict).post("/books", content=body).status_code)
print(TestClient(strict).post("/books", content=body, headers={"Content-Type": "application/json"}).status_code)
print(TestClient(lenient).post("/books", content=body).status_code)
422
200
200
from collections.abc import AsyncIterable
from fastapi import FastAPI
from fastapi.sse import EventSourceResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class Book(BaseModel):
title: str
@app.get("/books/stream")
async def stream_books() -> AsyncIterable[Book]:
yield Book(title="Dune")
yield Book(title="Emma")
@app.get("/books/events", response_class=EventSourceResponse)
async def book_events() -> AsyncIterable[Book]:
yield Book(title="Dune")
client = TestClient(app)
lines = client.get("/books/stream")
print(lines.headers["content-type"], lines.text.splitlines())
events = client.get("/books/events")
print(events.headers["content-type"], events.text.strip().splitlines())
Source: FastAPI: Stream JSON Lines
application/jsonl ['{"title":"Dune"}', '{"title":"Emma"}']
text/event-stream; charset=utf-8 ['data: {"title":"Dune"}']
Server-Sent Events are described on Server-Sent Events and SPA serving on Frontend.
import tempfile
from pathlib import Path
from fastapi import FastAPI
from fastapi.testclient import TestClient
dist = Path(tempfile.mkdtemp())
(dist / "index.html").write_text("<html><body>SPA</body></html>")
app = FastAPI()
@app.get("/api/books")
def list_books() -> list[str]:
return ["Dune"]
app.frontend("/", directory=str(dist))
client = TestClient(app)
print(client.get("/api/books").json())
print(client.get("/", headers={"Accept": "text/html"}).text)
print(client.get("/books/42", headers={"Accept": "text/html"}).text)
['Dune']
<html><body>SPA</body></html>
<html><body>SPA</body></html>
Native OpenTelemetry (0.142.0)
Release 0.142.0 (2026-09-29) is the newest feature in the tables. Older guidance wires up the
opentelemetry-instrumentation-fastapi package by hand. Native support needs only an endpoint, supplied through
environment variables:
export OTEL_SERVICE_NAME=bookshelf-api
export OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com
fastapi run
Source: FastAPI: OpenTelemetry
Customization is the telemetry argument of FastAPI(…). This example sends spans to an in-memory exporter so it can
run without a collector:
from fastapi import FastAPI
from fastapi.testclient import TestClient
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
app = FastAPI(telemetry={"tracer_provider": provider})
@app.get("/books/{book_id}")
def read_book(book_id: int) -> dict[str, int]:
return {"book_id": book_id}
TestClient(app).get("/books/1")
print(sorted({span.name for span in exporter.get_finished_spans()}))
Source: FastAPI: OpenTelemetry
['GET /books/{book_id}', 'fastapi.dependencies', 'fastapi.endpoint', 'fastapi.serialization']
Sending the data to a real collector was not run here, because it needs one.
Corrected Forms for Frequent Mistakes
Printed books and tutorials contain code that never worked, and mistakes copy easily from one source to another. These are the correct forms of the most frequent ones, so you can recognise and fix them in older code:
-
Types come from
typingor the standard library, and models frompydantic;stris the built-in string type, andBaseModelis the base class for models. -
Table metadata comes from SQLModel or SQLAlchemy as documented on SQL Databases with SQLModel.
-
Streaming a large file means yielding it in chunks (see Streaming, JSON Lines & SSE), not reading the whole file into memory first.
-
Every import should be checked against the official reference, and every example should run before it is trusted, which is how the examples on this page were prepared.
Upgrade Checklist
-
Pin FastAPI and read the release notes between your current and target versions, concentrating on entries marked breaking (0.106.0, 0.132.0, 0.137.0 among those listed above).
-
Move to
fastapi[standard]and thefastapicommand; dropfastapi-slim. -
Remove Pydantic v1 code before upgrading past 0.126.0, using the Pydantic migration guide and
bump-pydantic. -
Replace
on_eventwithlifespan,python-joseandpasslibwith PyJWT and pwdlib, andutcnow()withdatetime.now(timezone.utc). -
Check tests that expected
403for missing credentials, and clients that send JSON without a JSONContent-Type. -
Move
Query()andDepends()defaults toAnnotatedgradually; both styles keep working. -
Re-run your tests with a warning filter set to error to find deprecated calls such as
ORJSONResponseearly.