Real payloads are nested and partly optional; the annotation is the whole specification.
Pydantic understands the standard typing vocabulary, so the annotation is the schema:
- scalars:
int,float,str,bool,bytes,Decimal,datetime,date,UUID,Path - collections:
list[int],set[str],tuple[int, int],dict[str, float] - choices:
Literal["draft", "published"]andEnumsubclasses - other models: a field typed
Addressvalidates a nested dict into anAddressinstance
Optional is not the same as not required. nickname: str | None means the value may be None but must still be provided. To make it optional, give a default: nickname: str | None = None.
Containers validate every element, and errors point at the exact position, such as items.2.price. Tuples check their length; tuple[int, ...] accepts any length.
Lax conversion is sensible rather than permissive: "2024-05-01" becomes a date, "true", "yes" and 1 become True, and an int field accepts 3.0 but rejects 3.5, because that would lose data. A str field does not accept an int; Pydantic will not silently stringify numbers.
datetime fields accept ISO 8601 strings and Unix timestamps. Use AwareDatetime if you must reject timestamps without a timezone.
Types from pydantic such as EmailStr, HttpUrl, PositiveInt and SecretStr add common checks; EmailStr needs the extra pip install "pydantic[email]".
Example
from datetime import date
from typing import Literal
from pydantic import BaseModel
class Address(BaseModel):
city: str
zip: str
class Order(BaseModel):
id: int
status: Literal["new", "paid", "shipped"] = "new"
placed: date
note: str | None = None
items: list[tuple[str, int]]
ship_to: Address
o = Order.model_validate({
"id": 1, "placed": "2024-05-01",
"items": [["pen", 2], ["ink", "1"]],
"ship_to": {"city": "Oslo", "zip": "0150"},
})
print(o.placed, o.items, o.ship_to.city)
# 2024-05-01 [('pen', 2), ('ink', 1)] OsloRemember
- The type hint is the schema: collections, Literal, Enum and nested models all validate
- X | None allows None; only a default makes a field optional
- Lax mode converts without losing data: 3.0 -> 3 is fine, 3.5 -> int fails
Watch out for
- Writing x: str | None and expecting it to be optional; it is still required.