app.patch()
| Since: | FastAPI 0.1(2019) |
|---|
In FastAPI, you use the @app.patch() decorator to define a path operation that handles HTTP PATCH requests. PATCH is used to update only specific fields of a resource, leaving any fields you do not send unchanged. By combining it with Pydantic's model_copy(update=...) and exclude_unset=True, you can implement partial updates in a type-safe way.
Syntax
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
# Schema for the request body: make all fields Optional for partial updates
class ModelName(BaseModel):
field_name: Optional[type] = None
# Basic syntax: specify the path and response model
@app.patch('/path/{path_param}', response_model=ResponseModel, status_code=200)
async def handler_function(path_param: type, body: ModelName):
# Use exclude_unset=True to get only the fields the client actually sent
update_data = body.model_dump(exclude_unset=True)
# Apply only the changed fields to the existing data using model_copy(update=...)
return existing_data.model_copy(update=update_data)
Parameter List
| Parameter | Description |
|---|---|
| path | Specifies the URL path for the endpoint as a string. You can embed path parameters using the {variable_name} syntax. |
| response_model | Specifies the response data type as a Pydantic model. When set, the response is automatically validated and serialized, and the schema is also reflected in the OpenAPI documentation. |
| status_code | Specifies the HTTP status code to return on success as an integer. Defaults to 200 if omitted. |
| tags | Specifies a list of tag names used to group the endpoint in the OpenAPI documentation. |
| summary | Specifies a short description of the endpoint to display in the OpenAPI documentation as a string. |
| description | Specifies a detailed description of the endpoint to display in the OpenAPI documentation as a Markdown-formatted string. |
| response_description | Specifies the description of the response to display in the OpenAPI documentation as a string. Defaults to "Successful Response". |
| deprecated | Specifies as a boolean whether to mark the endpoint as deprecated. Defaults to False. |
| operation_id | Specifies the operation ID in the OpenAPI specification as a string. If omitted, it is automatically generated from the function name. |
| dependencies | Specifies a list of Depends() to apply as dependency injections. Useful for registering dependencies whose return values are not needed, such as authentication checks. |
Sample Code
Using a user management API as an example, this defines an endpoint with @app.patch() that partially updates only the specified fields. The pattern uses exclude_unset=True to extract only the fields the client sent, and model_copy(update=...) to apply the diff to the existing data.
main.py
# ---- main.py ----
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
# Full schema for a user stored in the database
class User(BaseModel):
id: int
name: str
email: str
age: int
# Schema for the PATCH request body
# All fields are Optional so each one can be omitted for partial updates
class UserPatch(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
age: Optional[int] = None
# Dummy data store (a real app would use a database)
users_db: dict[int, User] = {
1: User(id=1, name='Kiryu Kazuma', email='kiryu@kamurocho.com', age=37),
2: User(id=2, name='Majima Goro', email='majima@kamurocho.com', age=38),
}
# Endpoint for partial user updates
# Receives the user ID from the URL via {user_id} and only the fields to update from the request body
@app.patch(
'/users/{user_id}',
response_model=User, # Formats the response using the User model
tags=['users'], # Groups the endpoint under "users" in the OpenAPI docs
summary='Partially update a user',
)
async def patch_user(user_id: int, patch: UserPatch):
# Return a 404 error if the target user does not exist
if user_id not in users_db:
raise HTTPException(status_code=404, detail='User not found.')
existing = users_db[user_id]
# exclude_unset=True: retrieves only the fields the client actually sent as a dict
# (omitted fields are treated as absent, not as None)
update_data = patch.model_dump(exclude_unset=True)
# model_copy(update=...): creates a new instance with only the changed fields applied to the existing User model
# Fields that were not sent retain their existing values
updated = existing.model_copy(update=update_data)
# Save the updated user to the data store
users_db[user_id] = updated
# The return value is automatically validated and serialized because response_model=User is set
return updated
# For testing: endpoint to retrieve a list of all users
@app.get('/users', response_model=list[User], tags=['users'])
async def list_users():
return list(users_db.values())
Common mistake 1: Omitting exclude_unset=True
If you omit exclude_unset=True, fields the client did not send will be included as None. This causes a bug where existing database values are overwritten with None.
NG — exclude_unset=True is omitted
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
class UserPatch(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
users_db = {
1: User(id=1, name='Kiryu Kazuma', email='kiryu@kamurocho.com'),
2: User(id=2, name='Majima Goro', email='majima@kamurocho.com'),
}
@app.patch('/users/{user_id}', response_model=User)
async def patch_user(user_id: int, patch: UserPatch):
if user_id not in users_db:
raise HTTPException(status_code=404, detail='User not found.')
existing = users_db[user_id]
update_data = patch.model_dump()
updated = existing.model_copy(update=update_data)
users_db[user_id] = updated
return updated
In the code above, even if you only send name, email is still included as None in update_data, which overwrites the existing email value.
OK — Use model_dump(exclude_unset=True) to retrieve only the fields that were sent
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
class UserPatch(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
users_db = {
1: User(id=1, name='Kiryu Kazuma', email='kiryu@kamurocho.com'),
2: User(id=2, name='Majima Goro', email='majima@kamurocho.com'),
}
@app.patch('/users/{user_id}', response_model=User)
async def patch_user(user_id: int, patch: UserPatch):
if user_id not in users_db:
raise HTTPException(status_code=404, detail='User not found.')
existing = users_db[user_id]
update_data = patch.model_dump(exclude_unset=True)
updated = existing.model_copy(update=update_data)
users_db[user_id] = updated
return updated
Common mistake 2: Forgetting to make PATCH schema fields Optional
Because PATCH is a partial update, clients must be able to send only the fields they want to change. If you forget to add Optional to fields in the request body schema, all fields become required and partial updates are no longer possible.
NG — PATCH schema fields are not Optional
from pydantic import BaseModel
class UserPatch(BaseModel):
name: str
email: str
users_db = {
1: {"id": 1, "name": "Kiryu Kazuma", "email": "kiryu@kamurocho.com"},
2: {"id": 2, "name": "Majima Goro", "email": "majima@kamurocho.com"},
}
In the code above, both name and email are required, so trying to update only name returns a validation error.
OK — Make all fields Optional with a default value of None
from pydantic import BaseModel
from typing import Optional
class UserPatch(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
users_db = {
1: {"id": 1, "name": "Kiryu Kazuma", "email": "kiryu@kamurocho.com"},
2: {"id": 2, "name": "Majima Goro", "email": "majima@kamurocho.com"},
}
Overview
@app.patch() is a path operation decorator for the HTTP PATCH method. The key difference between PATCH and PUT is the scope of the update. While @app.put() replaces an entire resource, @app.patch() applies a diff update using only the fields the client sends. Fields that are not sent retain their existing values.
To implement partial updates correctly, all fields in the Pydantic model for the request body must be set to Optional with a default value of None. Then, using model_dump(exclude_unset=True) lets you retrieve only the fields the client actually sent as a dict. If you omit exclude_unset=True, omitted fields will be included as None, which can cause a bug that unintentionally overwrites existing values. Pass the resulting diff to model_copy(update=...) on the existing model to generate a new instance that preserves all unchanged fields.
For a general overview of how to define endpoints, see @app.get(). Use @app.post() to create new resources, @app.put() for full replacements, and @app.delete() for deletions.
If you find any errors or copyright issues, please contact us.