Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 26

Building an Update Endpoint

Recommended resources

Hello! Let's continue our journey building a full CRUD application with FastAPI.

In our last lesson, we successfully implemented the "Create" part of CRUD, allowing us to add new items to our database using a POST endpoint. We now have a way to get data in. The next logical step is to manage that data by modifying it.

This lesson is all about the "U" in CRUD: Update. You will learn how to implement an endpoint that can modify an existing record in the database. This is a crucial feature for almost any API, whether it's for editing a user profile, changing a product's price, or updating the status of a task.

We'll explore the two standard HTTP methods for updates: PUT for complete replacement and PATCH for partial modification. We will focus primarily on PATCH, as it is often more flexible and practical for real-world APIs.

PUT vs. PATCH: Two Flavors of Update

When it comes to updating resources in a REST API, there are two distinct methods, and understanding their difference is key to designing a good API.

  • PUT: Used for a full update or replacement. When you use PUT, you are expected to send the complete representation of the resource. If you omit a field, the server should interpret that as an intent to remove or reset that field's value.
  • PATCH: Used for a partial update. With PATCH, you only need to send the fields you want to change. Any fields you omit should remain untouched in the database.

For example, if you have an item with a name and a price, and you only want to change the price, a PATCH request would just send {"price": 15.99}. A PUT request would require you to send both the new price and the existing name, like {"name": "My Item", "price": 15.99}.

The following short video clip explains this distinction clearly.

Python FastAPI Tutorial (Part 6): Completing CRUD - Update and Delete (PUT, PATCH, DELETE)

Watch this clip from Corey Schafer's Python FastAPI Tutorial to get a solid grasp of the conceptual difference between the PUT and PATCH methods.

Watch from 01:10 to 02:03. The video explains why PATCH is often more useful in practice, which is why we will focus on it first.


Implementing a PATCH Endpoint for Partial Updates

Because partial updates are so common, let's build a PATCH endpoint first. The process involves four main steps:

  1. Define a new Pydantic schema for updates where all fields are optional.
  2. Create the endpoint that fetches the existing record from the database.
  3. Dynamically apply only the changes provided in the request.
  4. Commit the changes and return the updated record.

1. A Schema for Partial Updates

To accept partial data, we need a new Pydantic model where every field is optional. This signals to FastAPI that a client can send any subset of the item's fields. We can achieve this using Python's Optional type hint and setting a default value of None.

In your schemas.py file, add the following model:




# In schemas.py
from typing import Optional
from pydantic import BaseModel




# ... your other schemas (ItemBase, ItemCreate, Item) ...

class ItemUpdate(BaseModel):
    name: Optional[str] = None
    description: Optional[str] = None
    price: Optional[float] = None
    tax: Optional[float] = None

This ItemUpdate schema tells FastAPI that a request to update an item can contain a name, a price, both, or neither. Any fields not included in the request body will not be part of the validated data.

The video you just saw also demonstrates creating this exact type of schema for updates.

Python FastAPI Tutorial (Part 6): Completing CRUD - Update and Delete (PUT, PATCH, DELETE)

This next clip from the same video shows how to create a Pydantic schema specifically for partial updates by making all fields optional.

Watch from 02:03 to 03:48. Notice how Optional[str] is used and how a default value of None is set. This is the standard pattern for an update schema.

2. The PATCH Endpoint Logic

Now let's build the endpoint in your main.py. The core of the logic is to fetch the existing item, then carefully apply only the fields that the client sent in the request.

Here is the complete path operation for a partial update:




# In main.py
# ... other imports
from schemas import ItemUpdate # Make sure to import your new schema




# ... your app setup and other endpoints ...

@app.patch("/items/{item_id}", response_model=schemas.Item)
async def update_item_partial(
    item_id: int, 
    item: ItemUpdate, 
    db: AsyncSession = Depends(get_db)
):



    # 1. Get the existing item from the database
    db_item = await db.get(models.Item, item_id)
    if not db_item:
        raise HTTPException(status_code=404, detail="Item not found")




    # 2. Get the request data as a dictionary, excluding any fields that were not set
    update_data = item.model_dump(exclude_unset=True)




    # 3. Loop through the update data and update the database object
    for key, value in update_data.items():
        setattr(db_item, key, value)




    # 4. Add, commit, and refresh the database object
    db.add(db_item)
    await db.commit()
    await db.refresh(db_item)
    
    return db_item

Let's break down the most important new parts:

  • @app.patch(...): We use the @app.patch decorator for partial updates. The path includes the {item_id} to identify which resource to modify.
  • item: ItemUpdate: The request body is validated against our new ItemUpdate schema.
  • db_item = await db.get(models.Item, item_id): We use the db.get() convenience method (available in modern SQLAlchemy) to fetch the item by its primary key. It's a clean way to perform this common lookup. If it doesn't find the item, it returns None.
  • update_data = item.model_dump(exclude_unset=True): This is the key to making partial updates work. item.model_dump() converts the Pydantic model to a dictionary. The argument exclude_unset=True tells Pydantic to only include key-value pairs that were explicitly sent in the request body. Fields that were not sent are omitted from the dictionary, preventing us from overwriting existing data with None.
  • setattr(db_item, key, value): We loop through the update_data dictionary and use Python's built-in setattr() function to dynamically update the attributes on our db_item SQLAlchemy model. This is equivalent to doing db_item.name = "new name", but in a flexible way that works for any field.
  • db.add(db_item): When you add an object that already exists in the session (because we just fetched it), SQLAlchemy is smart enough to know that it should be marked for an UPDATE operation, not an INSERT.

The following video walks through this entire implementation, including the model_dump(exclude_unset=True) and setattr logic.

Python FastAPI Tutorial (Part 6): Completing CRUD - Update and Delete (PUT, PATCH, DELETE)

This final clip from Corey Schafer's tutorial provides a complete walkthrough of implementing and testing the PATCH endpoint. It's a great visual guide for the code we just wrote.

Watch from 10:36 to 15:45. Pay close attention to the explanation of model_dump(exclude_unset=True) and the for loop with setattr. The video then shows how to test the endpoint by sending only a single field to update.

For a supplementary text-based resource, the following article provides an excellent, clean example of this entire process using an asynchronous setup similar to ours.

Connecting FastAPI to a Database with SQLModel - David Muraya

The article "Connecting FastAPI to a Database with SQLModel" by David Muraya provides a well-structured, asynchronous example of CRUD operations. The update logic is particularly relevant.

Please read the following three parts of the article: Section 4, ProductUpdate schema: Notice how it defines all fields as optional. Section 5, update_product function: This function contains the core database logic, almost identical to what we've implemented. Section 6, @app.patch endpoint: This shows how the endpoint ties everything together. Focus on how the code is structured and how it handles the partial update.

FastAPI Interactive Documentation for Student CRUD Operations
This image from a sample project shows how an update endpoint (`PUT` in this case, but `PATCH` would look similar) appears in the interactive documentation, ready for testing.

A Note on Full Updates with PUT

While PATCH is often more practical, you might encounter situations where a full replacement with PUT is required. The implementation is simpler because you don't need to dynamically decide which fields to update.

To implement a PUT endpoint, you would:

  1. Use the @app.put("/items/{item_id}", ...) decorator.
  2. Use the schemas.ItemCreate model for the request body, as it requires all the necessary fields for a full replacement.
  3. After fetching the db_item, you would manually assign every field from the request body to the database object:
    db_item.name = item.name
    db_item.description = item.description
    db_item.price = item.price
    db_item.tax = item.tax
    
  4. Finally, you would commit and refresh as usual.

This approach is less flexible for clients but can be useful for ensuring a resource is completely overwritten with new data.


Conclusion

Congratulations! You've now implemented the "Update" functionality in your API, giving you the power to modify existing data. This is a significant milestone that moves us closer to a fully functional application.

Key Takeaways:

  • PATCH is for partial updates, where the client sends only the fields to be changed.
  • PUT is for full updates, where the client replaces the entire resource.
  • For PATCH, you need a dedicated Pydantic schema with all optional fields.
  • The model_dump(exclude_unset=True) method is essential for getting only the data that was explicitly sent in a PATCH request.
  • Using a loop with setattr() is a clean, dynamic way to apply partial updates to your SQLAlchemy model.

In our next and final lesson of this module, we will complete the CRUD cycle by implementing the "Delete" functionality.