Pydantic Models and Validation
22 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Data that comes from outside cannot be trusted. Pydantic models describe the shape you expect, convert what they can and reject the rest. FastAPI builds on them for both requests and responses.
A model is a class that checks its data
Pydantic model is a class that inherits from BaseModel. It converts and checks its fields when an object is created and raises ValidationError when the data is wrong.
Pydantic is the library FastAPI uses to check data. You describe the data as a class with type hints. When you create an object, Pydantic converts what it can, such as the text "5" to the number 5, and raises an error for what it cannot.
from pydantic import BaseModel
class Product(BaseModel): name: str price: float stock: int = 0
item = Product(name="Keyboard", price="49.5")print(item.price, item.stock)Example 1 · Conversion and ValidationError
from pydantic import BaseModel, ValidationError
class Product(BaseModel): name: str price: float stock: int = 0
item = Product(name="Keyboard", price="49.5")print(item)print(item.model_dump())
try: Product(name="Mouse", price="cheap")except ValidationError as err: problem = err.errors()[0] print(problem["loc"], problem["type"]) print(err.error_count())name='Keyboard' price=49.5 stock=0
{'name': 'Keyboard', 'price': 49.5, 'stock': 0}
('price',) float_parsing
1What this code does
price="49.5"is a string, but it is converted to a float.- The error says where (
loc) and what kind (type) without printing a long message.
The text "49.5" became the float 49.5, and stock took its default 0. A value like price="cheap" would raise a ValidationError.
Field adds rules to one value
Field is a Pydantic function that attaches rules (such as min_length, ge, le, pattern) and a description to one model field.
A type says what kind of value is allowed. Field says more: a minimum length, a range, a pattern or a description. Put it as the default value, or inside Annotated.
from pydantic import BaseModel, Field
class Student(BaseModel): name: str = Field(min_length=2, max_length=40) marks: int = Field(ge=0, le=100) email: str = Field(pattern=r"^[^@ ]+@[^@ ]+$") tags: list[str] = Field(default_factory=list)Example 2 · Field rules and several errors at once
from pydantic import BaseModel, Field, ValidationError
class Student(BaseModel): name: str = Field(min_length=2, max_length=40) marks: int = Field(ge=0, le=100) tags: list[str] = Field(default_factory=list)
a = Student(name="Ravi", marks=82)b = Student(name="Meena", marks=90)a.tags.append("topper")print(a.tags, b.tags)
try: Student(name="X", marks=140)except ValidationError as err: for e in err.errors(): print(e["loc"][0], e["type"])['topper'] [] name string_too_short marks less_than_equal
What this code does
- Each object gets its own list from
default_factory, sob.tagsstays empty. - Pydantic reports all the problems, not only the first.
default_factory=list gives each object its own new list. Never write tags: list = [] by habit; use the factory.
What a 422 answer contains
When the body of a request does not fit the model, FastAPI answers 422 and lists every problem. Each problem has loc (where the bad value is), msg (a readable message) and type (a short code). You can add your own rule with field_validator.
from pydantic import field_validator
class OrderIn(BaseModel): item: str qty: int = Field(ge=1)
@field_validator("item") @classmethod def no_blank(cls, value): if not value.strip(): raise ValueError("item must not be blank") return value.strip()Example 3 · A 422 answer and a custom rule
from fastapi import FastAPIfrom fastapi.testclient import TestClientfrom pydantic import BaseModel, Field, field_validator
app = FastAPI()
class OrderIn(BaseModel): item: str qty: int = Field(ge=1)
@field_validator("item") @classmethod def no_blank(cls, value): if not value.strip(): raise ValueError("item must not be blank") return value.strip()
@app.post("/orders")def create_order(order: OrderIn): return order
client = TestClient(app)print(client.post("/orders", json={"item": " Router ", "qty": 2}).json())r = client.post("/orders", json={"item": " ", "qty": 0})print(r.status_code)for e in r.json()["detail"]: print(e["loc"], "|", e["msg"]){'item': 'Router', 'qty': 2}
422
['body', 'item'] | Value error, item must not be blank
['body', 'qty'] | Input should be greater than or equal to 1What this code does
- The valid call comes back with the spaces removed by the validator.
- Both problems are reported in one 422 answer, in field order.
A validator may change the value it returns. Here it strips spaces. A ValueError becomes a 422 message that starts with "Value error,".
response_model controls what leaves the API
response_model is a route setting that names the model for the answer. FastAPI keeps only its fields, converts the types and documents the shape.
The model you accept is often not the model you want to send. A user sends a password, but the answer must never include it. Set response_model= on the route, and FastAPI filters the returned data through that model.
class UserIn(BaseModel): email: str password: str
class UserOut(BaseModel): id: int email: str
@app.post("/users", response_model=UserOut, status_code=201)def create_user(user: UserIn): return {"id": 1, "email": user.email, "password": user.password}Example 4 · Keep the password out of the answer
from fastapi import FastAPIfrom fastapi.testclient import TestClientfrom pydantic import BaseModel
app = FastAPI()
class UserIn(BaseModel): email: str password: str
class UserOut(BaseModel): id: int email: str
@app.post("/users", response_model=UserOut, status_code=201)def create_user(user: UserIn): return {"id": 1, "email": user.email, "password": user.password}
r = TestClient(app).post("/users", json={"email": "a@shop.test", "password": "s3cret"})print(r.status_code)print(r.json())print("password" in r.json())201
{'id': 1, 'email': 'a@shop.test'}
FalseWhat this code does
- The route returns a dict with a password, and
response_modelremoves it. - Use two models, one for input and one for output, so secrets cannot leak by mistake.
Example 5 · Nested models and lists in the answer
from fastapi import FastAPIfrom fastapi.testclient import TestClientfrom pydantic import BaseModel
app = FastAPI()
class Address(BaseModel): city: str pin: str
class CustomerOut(BaseModel): id: int name: str address: Address
data = [ {"id": 1, "name": "Asha", "address": {"city": "Pune", "pin": "411001"}, "internal_note": "vip"}, {"id": 2, "name": "Ravi", "address": {"city": "Delhi", "pin": "110001"}, "internal_note": "late payer"},]
@app.get("/customers", response_model=list[CustomerOut])def list_customers(): return data
body = TestClient(app).get("/customers").json()print([c["address"]["city"] for c in body])print(sorted(body[0]))['Pune', 'Delhi'] ['address', 'id', 'name']
What this code does
list[CustomerOut]filters every item in the list.internal_noteis gone from the answer because the model does not have it.
The function returns a password, but the client never sees it, because UserOut has no such field. It also adds the output shape to the docs.
Next step
Check your understanding
12 quick questions