Write the Save Function
Create a function that writes expenses and the counter to a JSON file
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
Converting Python data to JSON
Python's json.dumps() function converts a dictionary into a JSON-formatted string. The indent=2 argument adds line breaks and spacing so the file is readable:
json.dumps({"name": "Lunch"}, indent=2)
# '{\n "name": "Lunch"\n}'Writing to a file with pathlib
The pathlib module provides Path objects that represent file paths. Path.write_text() writes a string to a file, creating the file if it does not exist:
from pathlib import Path
Path("data.json").write_text("hello")What to save
You need to save two things: the expenses dictionary and the counter. Without the counter, the server would reset its identifier sequence after a restart. Two expenses could end up with the same identifier.
The save function wraps both values in a single dictionary and writes it to the file.
Instructions
Create the save_expenses function between the models and the expenses = {} line.
- Define a function named
save_expensesthat takes no arguments. - Inside the function, create a dictionary named
datawith two keys:"counter"set tocounterand"expenses"set toexpenses. - Call
DATA_FILE.write_text(json.dumps(data, indent=2))to write the data to the file.
import json
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, field_validator
from typing import Literal, Optional
app = FastAPI()
DATA_FILE = Path("expenses.json")
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
# Step 1: Define a function named save_expenses
# Step 2: Create data = {"counter": counter, "expenses": expenses}
# Step 3: Write DATA_FILE.write_text(json.dumps(data, indent=2))
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)
@app.patch("/expenses/{expense_id}")
def update_expense(expense_id: int, updates: ExpenseUpdate):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
expenses[expense_id].update(updates.model_dump(exclude_unset=True))
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