The ExpenseUpdate Model
Create a Pydantic model where every field is optional for partial updates
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
Why a separate model?
The Expense model requires description, amount, and category. That is correct for creating an expense — every new record needs those fields. But for updates, requiring all fields defeats the purpose of PATCH. A client fixing only the amount should not need to resend the description and category.
You need a second model where every field is optional. If the client sends a field, Pydantic validates it. If the client omits a field, Pydantic marks it as unset.
Making fields optional with defaults
Setting default=None inside Field() makes a field optional while keeping its validation constraint active:
amount: Optional[float] = Field(default=None, gt=0)- If the client sends
"amount": 15.0, Pydantic checks that 15.0 is greater than 0. Valid. - If the client sends
"amount": -5, Pydantic rejects it. Thegt=0rule still applies. - If the client omits
amountentirely, the value isNoneand Pydantic skips validation.
This pattern gives you optional fields with the same safety rules as the original model.
Instructions
Create the ExpenseUpdate model below the Expense class.
- Create a class named
ExpenseUpdatethat inherits fromBaseModel. - Add
descriptionasOptional[str]withField(default=None, min_length=1). - Add
amountasOptional[float]withField(default=None, gt=0). - Add
categoryasOptional[Literal["food", "transport", "entertainment", "utilities", "other"]]with a default ofNone.
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
# Step 1: Create a class named ExpenseUpdate that inherits from BaseModel
# Step 2: Add description as Optional[str] with Field(default=None, min_length=1)
# Step 3: Add amount as Optional[float] with Field(default=None, gt=0)
# Step 4: Add category as Optional[Literal[...]] with default 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)
Interactive Code Editor
Sign in to write and run code, track your progress, and unlock all chapters.
Sign In to Start Coding