Using TypeDicts in FastAPI for PATCH endpoints
This article goes through a quick example of how to use TypedDict in FastAPI for the body of a PATCH endpoint.
Update: October, 2025
Starting with Pydantic 2.12.0, the MISSING sentinel can be used to annotate non-required, non-nullable fields, which is very useful for PATCH endpoints.
Read my other blog post for more details.
Consider a FastAPI application with the following endpoints
GET /movies: Get all moviesGET /movies/{movie_id}: Get a movie by ID
Here's what the output of the latter endpoint might look like
{
"comment": null,
"id": 1,
"rating": null,
"title": "Pulp Fiction",
"year": 1994
}
and let's say we want to add the following endpoint:
PATCH /movies/{movie_id}: Update a movie's rating or comment
The HTTP verb PATCH makes the most sense for this operation, as it's used to apply partial modifications to a resource. The request body should contain only the fields that need to be updated.
Let's look at different implementations and point out the issues with each.
Almost there: Pydantic model with extra="forbid"
The first solution that might come to mind is adding a Pydantic model with the fields that can be updated, and setting extra="forbid" to prevent any other fields from being passed.
from pydantic import BaseModel, ConfigDict
class MovieUpdate(BaseModel):
model_config = ConfigDict(extra="forbid")
rating: Rating
comment: Comment
@app.patch("/movies/{movie_id}")
def update_movie(movie_id: int, update: schemas.MovieUpdate) -> schemas.MovieRead:
if movie := db.get(doc_id=movie_id):
movie.update(update.model_dump())
db.update(movie, doc_ids=[movie_id])
return schemas.MovieRead.model_validate({"id": movie.doc_id, **movie})
raise HTTPException(
status_code=http.HTTPStatus.NOT_FOUND,
detail="Movie not found",
)
This approach will correctly disallow updating fields that are not in the MovieUpdate model, such as title and year, but fields are required so we cannot omit them from the request body.
We could make the fields optional by setting None as the default value:
class MovieUpdate(BaseModel):
model_config = ConfigDict(extra="forbid")
rating: Rating | None = None
comment: Comment | None = None
This is not ideal because it allows None as a valid value for the fields, which is not what we want.
The solution: TypedDict
The TypedDict class was introduced in Python 3.8. Pydantic has support for it, and it's perfect for this use case.
from typing import TypedDict
from pydantic import ConfigDict, with_config
class MovieUpdate(TypedDict, total=False, closed=True): # (1) (2)
rating: Rating
comment: Comment
@app.patch("/movies/{movie_id}")
def update_movie(movie_id: int, update: schemas.MovieUpdate) -> schemas.MovieRead:
if movie := db.get(doc_id=movie_id):
movie.update(update)
db.update(movie, doc_ids=[movie_id]) # (3)
return schemas.MovieRead.model_validate({"id": movie.doc_id, **movie})
raise HTTPException(
status_code=http.HTTPStatus.NOT_FOUND,
detail="Movie not found",
)
- Use
total=Falseto make all fields optional. - Use
closed=Trueto disallow extra fields. - Since
movieis a dictionary, we can use it directly to update the fields.
Update: October, 2025
In a previous version of this article, I used Pydantic's @with_config(extra="forbid") decorator to disallow extra fields.
This is no longer necessary, as
- PEP 728 has been accepted
typing-extensionsalready backported it in version 4.10.0.- Pydantic supports it natively since version 2.12.0.
Please note that mypy doesn't support PEP 728 yet.
This gives us all the desired behavior:
- Fields are optional
{
"comment": null,
"id": 1,
"rating": 3,
"title": "Pulp Fiction",
"year": 1994
}
- Extra fields are not allowed
{
"detail": [
{
"input": 2024,
"loc": ["body", "year"],
"msg": "Extra inputs are not permitted",
"type": "extra_forbidden"
}
]
}
- Fields are not nullable
{
"detail": [
{
"input": null,
"loc": ["body", "rating"],
"msg": "Input should be a valid integer",
"type": "int_type"
}
]
}
Further reading
- TypedDicts are better than you think by Jamie Chang.
- PEP 728 - TypedDict with Typed Extra Items.
Appendix: PEP 728
Appendix: PEP 728
Update: September, 2025
PEP 728 has been accepted 🎉.
PEP 728 will allow us to declare the TypedDict with closed=True to disallow extra fields,
or extra_items=... to allow only fields of a certain type. It will be available in Python 3.15, scheduled for release in October 2026,
but typing-extensions already backported it in version 4.10.0.
When Pydantic supports it, the implementation would become simply:
- Use
total=Falseto make all fields optional, and useclosed=Trueto disallow extra fields.
Appendix: Full code
| my_app/app.py | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |