Day 10 · ~12m

Nested Models & Relationships

Composing models inside models for complex, hierarchical data structures.

🧑‍💻

So far all my models have had simple fields — strings, ints, bools. What if a field is itself a complex object, like an address inside a user?

👩‍🏫

You use a Pydantic model as the field type. Pydantic validates the nested model automatically:

from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

class User(BaseModel):
    name: str
    email: str
    address: Address

user = User(
    name="Alice",
    email="alice@example.com",
    address={"street": "123 Main St", "city": "Portland", "zip_code": "97201"}
)
print(user.address.city)  # "Portland"

Notice you can pass the address as a plain dictionary — Pydantic automatically converts it to an Address instance and validates all its fields.

🧑‍💻

What if the nested model has its own validators? Do those run too?

👩‍🏫

Yes. When Pydantic validates User, it validates Address first — including all its field constraints and @field_validator methods. Validation flows from the inside out:

from pydantic import BaseModel, Field, field_validator

class Address(BaseModel):
    street: str = Field(min_length=1)
    city: str = Field(min_length=1)
    zip_code: str

    @field_validator("zip_code")
    @classmethod
    def validate_zip(cls, v):
        if not v.isdigit() or len(v) != 5:
            raise ValueError("must be 5 digits")
        return v

class User(BaseModel):
    name: str
    address: Address

If the zip code is wrong, the error message tells you exactly where: address.zip_code.

🧑‍💻

Can I have a list of nested models? Like a user with multiple addresses?

👩‍🏫

Absolutely. Use list[Address] as the type:

class Customer(BaseModel):
    name: str
    addresses: list[Address]

customer = Customer(
    name="Bob",
    addresses=[
        {"street": "123 Main St", "city": "Portland", "zip_code": "97201"},
        {"street": "456 Oak Ave", "city": "Seattle", "zip_code": "98101"}
    ]
)
print(len(customer.addresses))       # 2
print(customer.addresses[0].city)    # "Portland"

Each dictionary in the list gets validated as an Address. If any one of them fails, Pydantic tells you which index had the problem.

🧑‍💻

How deep can the nesting go? Can I have models inside models inside models?

👩‍🏫

As deep as you need. Each level validates independently:

class GeoPoint(BaseModel):
    lat: float = Field(ge=-90, le=90)
    lng: float = Field(ge=-180, le=180)

class Location(BaseModel):
    name: str
    coordinates: GeoPoint

class Store(BaseModel):
    brand: str
    location: Location

store = Store(
    brand="Acme",
    location={
        "name": "Downtown",
        "coordinates": {"lat": 45.5, "lng": -122.6}
    }
)
print(store.location.coordinates.lat)  # 45.5

The key insight: break complex data into small, focused models, then compose them. Each model validates its own piece, and the whole structure is guaranteed correct.

Practice your skills

Sign up to write and run code in this lesson.

Already have an account? Sign in