Hello again! In this section, we are going to tackle data integrity. Building an API isn't just about moving data from A to B; it is about ensuring that the data is correct, safe, and structured.
FastAPI shines here because it automates the boring work of checking data types. Instead of writing dozens of if statements to check if an age is a number or an email is valid, we use Pydantic. We will also cover how to protect sensitive data (like passwords) from being sent back to the user and how to communicate properly using HTTP status codes.
In traditional frameworks, you often have to write manual validation logic (e.g., "is this field present?", "is it an integer?"). In FastAPI, validation is declarative and automatic.
You validate data by defining a Pydantic model and declaring it as a type hint in your path operation function.
When a request arrives, FastAPI does the following automatically:
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
# Define validation rules here
class UserSignup(BaseModel):
username: str
email: EmailStr # Requires 'pip install pydantic[email]'
age: int
@app.post("/signup/")
def signup(user: UserSignup):
# If we reach this line, 'user' is guaranteed to be valid.
# 'user.age' is definitely an int.
return {"message": f"User {user.username} validated successfully"}
Sample Output (Invalid Request):
{
"detail": [
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
}
]
}If FastAPI is the car, Pydantic is the engine. Pydantic modelsare Python classes that inherit from BaseModel. They represent the "shape" or "blueprint" of your data.
Analogy: The Cookie Cutter
Think of raw data (JSON) as dough. A Pydantic model is a cookie cutter. You press the cutter onto the dough. If the dough fits the shape, you get a perfect, structured object. If the dough is the wrong consistency (wrong data type), it doesn't work.
Key capabilities of Pydantic models:
int is an int..model_dump()) or JSON (.model_dump_json()).Interview note (Pydantic v1 vs v2): Pydantic v2 rewrote the validation core in Rust and renamed most of the public API. A lot of older tutorials still show the v1 names, so knowing the mapping is an easy way to show you have used it recently:
.dict() → .model_dump().json() → .model_dump_json().parse_obj() → .model_validate()class Config → model_config = ConfigDict(...)orm_mode → from_attributes@validator → @field_validator, and @root_validator → @model_validatorfrom pydantic import BaseModel, field_validator
class Product(BaseModel):
name: str
price: float
in_stock: bool = True # Default value
@field_validator("price")
@classmethod
def price_must_be_positive(cls, v: float) -> float:
if v <= 0:
raise ValueError("price must be greater than zero")
return v
# Creating an instance (Parsing)
external_data = {"name": "Smartphone", "price": "999.99"}
product = Product(**external_data)
print(product.price)
# Output: 999.99 (the string was coerced to a float)
print(product.model_dump())
# {'name': 'Smartphone', 'price': 999.99, 'in_stock': True}
Real-world data is rarely flat. You might have a user who has an address, and that address has a city and zip code. FastAPI handles this effortlessly using Nested Models.
You simply define a Pydantic model for the child (e.g., Address) and use it as a type hint inside the parent model (e.g., User). FastAPI understands this hierarchy and expects the incoming JSON to match it.
from typing import List
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Child Model
class Image(BaseModel):
url: str
name: str
# Parent Model
class Item(BaseModel):
name: str
description: str
# Nesting: An item has a list of Images
tags: List[str] = []
images: List[Image] = []
@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
return {"item_name": item.name, "image_count": len(item.images)}
This allows clients to send complex JSON structures like:
{
"name": "Keyboard",
"description": "Mechanical",
"tags": ["tech", "sale"],
"images": [
{"url": "http://img.com/1.png", "name": "Front View"},
{"url": "http://img.com/2.png", "name": "Side View"}
]
}A Response Model is used to define what data should be sent back to the client. This is critical for security.
The Problem: Your database object might contain a hashed_password field. If you just return the object directly, that secret data might be sent to the frontend.
The Solution: You define a Pydantic model that includes only the fields you want to share (e.g., username, email) and exclude the secrets. You pass this model to the response_model parameter in the decorator.
class UserIn(BaseModel):
username: str
password: str
email: str
class UserOut(BaseModel):
username: str
email: str
# Notice: 'password' is missing here
@app.post("/user/", response_model=UserOut)
def create_user(user: UserIn):
# We are returning the full 'user' object (which has the password)
# BUT, FastAPI will filter it through 'UserOut' before sending.
return user
Tip: This also automatically generates the correct schema for your API documentation, letting users know exactly what fields they will receive in the response.
HTTP status codes tell the client what happened. By default, FastAPI returns200 OK. However, if you create a new resource, you should return201 Created. If something is missing, you should return 404 Not Found.
There are two main ways to handle this:
status_code parameter in the decorator.HTTPException inside the function.from fastapi import FastAPI, status, HTTPException
app = FastAPI()
tasks = {"1": "Buy milk"}
# 1. Success: explicitly set 201 Created
@app.post("/tasks/", status_code=status.HTTP_201_CREATED)
def create_task(task_id: str, name: str):
tasks[task_id] = name
return {"task_id": task_id, "name": name}
# 2. Error: dynamically raise 404
@app.get("/tasks/{task_id}")
def read_task(task_id: str):
if task_id not in tasks:
raise HTTPException(
status_code=404,
detail="Task not found"
)
return {"task": tasks[task_id]}
Using status.HTTP_201_CREATED is preferred over just writing 201because it makes your code more readable and prevents typos.
3 questions · no sign-up, nothing stored
Progress is stored in this browser only - no sign-up, nothing sent anywhere.