Write the PATCH Endpoint
Add a PATCH route that applies partial updates to a stored expense
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
Merging changes into the stored expense
The PATCH endpoint follows the same pattern as GET and DELETE: extract the identifier from the URL, check that the expense exists, and raise a 404 if it does not.
The new part is applying the changes. The model_dump(exclude_unset=True) method returns a dictionary with only the fields the client sent. Python's dict.update() merges those key-value pairs into the stored expense:
# Client sends {"amount": 15.0}
changes = updates.model_dump(exclude_unset=True) # {"amount": 15.0}
expenses[expense_id].update(changes) # Only amount changesFields the client did not send remain untouched. The description, category, and date keep their original values.
Instructions
Add the PATCH endpoint at the bottom of the file.
- Add the decorator
@app.patch("/expenses/{expense_id}"). - Define a function named
update_expensewithexpense_id: intandupdates: ExpenseUpdateas parameters. - If
expense_idis not inexpenses, raiseHTTPException(status_code=404, detail="Expense not found"). - Call
expenses[expense_id].update(updates.model_dump(exclude_unset=True))and returnexpenses[expense_id].
from datetime import datetime
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, field_validator
from typing import Literal, Optional
app = FastAPI()
class Expense(BaseModel):
description: str = Field(min_length=1)
amount: float = Field(gt=0)
category: Literal["food", "transport", "entertainment", "utilities", "other"]
date: Optional[str] = None
@field_validator("date")
@classmethod
def validate_date_format(cls, v):
if v is not None:
try:
datetime.strptime(v, "%Y-%m-%d")
except ValueError:
raise ValueError("Date must be in YYYY-MM-DD format")
return v
class ExpenseUpdate(BaseModel):
description: Optional[str] = Field(default=None, min_length=1)
amount: Optional[float] = Field(default=None, gt=0)
category: Optional[Literal["food", "transport", "entertainment", "utilities", "other"]] = None
expenses = {}
counter = 0
@app.post("/expenses", status_code=201)
def create_expense(expense: Expense):
global counter
counter += 1
expenses[counter] = {"id": counter, **expense.model_dump()}
return expenses[counter]
@app.get("/expenses")
def list_expenses(category: str = None):
if category:
return [e for e in expenses.values() if e["category"] == category]
return list(expenses.values())
@app.get("/expenses/{expense_id}")
def get_expense(expense_id: int):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
return expenses[expense_id]
@app.delete("/expenses/{expense_id}")
def delete_expense(expense_id: int):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
return expenses.pop(expense_id)
# Step 1: Add @app.patch("/expenses/{expense_id}")
# Step 2: Define update_expense with expense_id: int and updates: ExpenseUpdate
# Step 3: If expense_id not in expenses, raise HTTPException(status_code=404, detail="Expense not found")
# Step 4: Apply changes and return expenses[expense_id]
Interactive Code Editor
Sign in to write and run code, track your progress, and unlock all chapters.
Sign In to Start Coding