Create the Settings Class
Define a Settings class that reads configuration from environment variables
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
How BaseSettings works
Pydantic's BaseSettings works like BaseModel, but instead of reading data from a request body, it reads data from environment variables. Each field in the class maps to an environment variable with the same name.
class Settings(BaseSettings):
data_file: str = "expenses.json"When you create a Settings() instance, Pydantic checks for an environment variable named DATA_FILE. If it finds one, it uses that value. If not, it falls back to the default value "expenses.json".
The .env file
Instead of setting environment variables in the shell, you can put them in a .env file in your project directory:
DATA_FILE=production_expenses.json
APP_TITLE=Expense Tracker APIAdding model_config = SettingsConfigDict(env_file=".env") tells Pydantic to read from this file automatically. Environment variables set in the shell take priority over values in the .env file.
Install pydantic-settings
BaseSettings lives in a separate package called pydantic-settings. You need to install it with pip install pydantic-settings before importing it. If you are following along in your own environment, run that command first.
Instructions
Add a Settings class to your API.
- Add
from pydantic_settings import BaseSettings, SettingsConfigDictto the imports. - After the models (after the
ErrorResponseclass), define a class calledSettingsthat inherits fromBaseSettings. Inside it, add:data_file: str = "expenses.json"app_title: str = "FastAPI"model_config = SettingsConfigDict(env_file=".env")
- After the class definition, create a
settingsinstance withsettings = Settings().
import json
import os
import time
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, field_validator
from typing import Literal, Optional
# Step 1: Add from pydantic_settings import BaseSettings, SettingsConfigDict
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
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
class ExpenseOut(BaseModel):
id: int
description: str
amount: float
category: str
date: Optional[str] = None
class ErrorResponse(BaseModel):
detail: str
# Step 2: Define class Settings(BaseSettings)
# data_file: str = "expenses.json"
# app_title: str = "FastAPI"
# model_config = SettingsConfigDict(env_file=".env")
# Step 3: Create settings = Settings()
def save_expenses():
temp_path = str(DATA_FILE) + ".tmp"
with open(temp_path, "w") as f:
json.dump({"counter": counter, "expenses": expenses}, f, indent=2)
os.replace(temp_path, str(DATA_FILE))
def load_expenses():
global expenses, counter
if DATA_FILE.exists():
data = json.loads(DATA_FILE.read_text())
expenses = {int(k): v for k, v in data["expenses"].items()}
counter = data["counter"]
expenses = {}
counter = 0
load_expenses()
@app.middleware("http")
async def log_requests(request, call_next):
start = time.time()
response = await call_next(request)
duration = (time.time() - start) * 1000
print(f"{request.method} {request.url.path} {response.status_code} {duration:.1f}ms")
return response
@app.post("/expenses", status_code=201, response_model=ExpenseOut)
def create_expense(expense: Expense):
global counter
counter += 1
expenses[counter] = {"id": counter, **expense.model_dump()}
save_expenses()
return expenses[counter]
@app.get("/expenses", response_model=list[ExpenseOut])
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}", response_model=ExpenseOut, responses={404: {"model": ErrorResponse}})
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}", response_model=ExpenseOut, responses={404: {"model": ErrorResponse}})
def delete_expense(expense_id: int):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
save_expenses()
return expenses.pop(expense_id)
@app.patch("/expenses/{expense_id}", response_model=ExpenseOut, responses={404: {"model": ErrorResponse}})
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))
save_expenses()
return expenses[expense_id]
@app.get("/summary")
def spending_summary():
summary = {}
for expense in expenses.values():
cat = expense["category"]
summary[cat] = summary.get(cat, 0) + expense["amount"]
return summary
Interactive Code Editor
Sign in to write and run code, track your progress, and unlock all chapters.
Sign In to Start Coding