Write the Load Function
Create a function that reads expenses from the JSON file on startup
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
Reading the file back
The save function writes data out. Now you need the reverse: a function that reads the JSON file and restores the expenses dictionary and counter.
json.loads() converts a JSON string back into a Python dictionary. Pair it with Path.read_text() to read the file contents:
data = json.loads(DATA_FILE.read_text())The string key problem
JSON only supports string keys. When you save {1: {"id": 1, ...}}, JSON converts the integer key 1 to the string "1". On load, you get {"1": {"id": 1, ...}} instead of {1: {"id": 1, ...}}.
Your endpoints use integer identifiers to look up expenses. If the keys are strings after loading, every lookup fails. The fix is to convert keys back to integers with int(k) during the load.
Handling first run
The first time the server starts, no JSON file exists yet. Calling read_text() on a missing file raises a FileNotFoundError. Check DATA_FILE.exists() first and skip loading if the file is not there. The expenses dictionary and counter keep their default values.
Instructions
Create the load_expenses function right after save_expenses.
- Define a function named
load_expensesthat takes no arguments. - Add
global expenses, counteras the first line inside the function. This lets the function modify the module-level variables. - Add an
if DATA_FILE.exists():check. Inside theifblock, read and parse the file:data = json.loads(DATA_FILE.read_text()). - Still inside the
ifblock, restore the data: setexpenses = {int(k): v for k, v in data["expenses"].items()}and setcounter = data["counter"].
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
def save_expenses():
data = {"counter": counter, "expenses": expenses}
DATA_FILE.write_text(json.dumps(data, indent=2))
# Step 1: Define a function named load_expenses
# Step 2: Add global expenses, counter
# Step 3: If DATA_FILE.exists(), read and parse: data = json.loads(DATA_FILE.read_text())
# Step 4: Restore: expenses = {int(k): v for k, v in data["expenses"].items()} and counter = data["counter"]
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