Field types, optionals and nesting

Pydantic · Essentials · 12 min

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"] and Enum subclasses
  • other models: a field typed Address validates a nested dict into an Address instance

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)] Oslo

Remember

  • 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.

Check yourself

  1. For class M(BaseModel): x: str | None, what does M() do?
  2. An int field receives 3.5. What happens in lax mode?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.pydantic.dev/latest/concepts/types/