Back to Blog
Python

Python Pydantic Nested Models, Lists, and Enums

python pydantic nested models lists and enums: Learn to define and validate nested models, lists of models, and enums in Python Pydantic schemas with clear examples.

pydanticdata validationnested modelsenumspython
Diagram showing a Pydantic model with nested model, list of models, and enum fields

When you need to model nested objects, lists of objects, and fixed-choice fields, Python Pydantic nested models lists and enums give you a declarative way to define and validate the entire structure. This is a common requirement for API request payloads, configuration files, and data pipelines. The following examples show how to declare these structures and how Pydantic validates them at runtime.

Defining a Nested Model

A nested model is simply a Pydantic model used as the type of a field in another model. Pydantic treats it like any other field type and instantiates and validates the inner model when the outer model is created.

from pydantic import BaseModel class Address(BaseModel): street: str city: str zip_code: str class User(BaseModel): name: str address: Address

When you create a User, the address field must be a dictionary or an Address instance. If you pass a dictionary, Pydantic converts it to an Address and validates its fields. If validation fails, the error is reported on the address field, and the nested error details are preserved.

Using Lists of Models

A list of models is declared by using the list type with the model as its parameter. Pydantic validates every element in the list against the model.

from pydantic import BaseModel class LineItem(BaseModel): sku: str quantity: int class Order(BaseModel): items: list[LineItem]

When you create an Order, each item in the items list must be a dictionary or a LineItem instance. Pydantic will validate each element independently and collect all errors. If one element is invalid, the whole Order creation fails, and the error message includes the index of the failing item.

Enums as Field Types

Enums restrict a field to a fixed set of allowed values. Pydantic accepts the enum member itself or its value, depending on how you define the enum.

from enum import Enum from pydantic import BaseModel class Status(str, Enum): pending = "pending" shipped = "shipped" delivered = "delivered" class Shipment(BaseModel): id: str status: Status

Using str as the base class makes the enum values strings, which is convenient for JSON serialization. When you create a Shipment, you can pass status="shipped" or status=Status.shipped. Pydantic will coerce the string to the enum member. If you pass an invalid value, validation fails with a clear error.

How Validation Propagates Through Nested Structures

Pydantic validates the entire object graph recursively when you create a model instance. This means that a nested model is validated when the outer model is constructed, and lists are validated element by element. The same rules apply to deeper nesting: a model inside a list inside another model is still fully validated.

This recursive behavior is useful because it catches invalid data at the boundary of your application, rather than deep inside business logic. However, it also means that a single invalid field in a large nested structure will cause the entire object creation to fail. If you need partial validation, you can use model_validate with partial or handle errors at the field level, but that is outside the scope of this article.

Handling Optional and Default Values

Nested models, lists, and enums can all be optional or have default values. Use Optional from typing or Pydantic's Field with a default.

from typing import Optional from pydantic import BaseModel, Field class Product(BaseModel): name: str tags: list[str] = Field(default_factory=list) category: Optional[str] = None

For nested models, a default can be a dictionary that Pydantic converts to the model type, but be careful with mutable defaults. Use default_factory for lists and dictionaries to avoid sharing state between instances.

For enums, you can set a default enum member:

class Order(BaseModel): status: Status = Status.pending

If a field is optional and no value is provided, Pydantic sets it to None. If a default is provided, that default is used. This is especially useful for API models where some fields are only present in certain responses.

Common Pitfalls with Nested Models and Enums

One common mistake is using a mutable default like [] for a list field. Pydantic actually prevents this and raises an error if you try, but it is still better to use default_factory. Another pitfall is confusing the enum member with its value. When you access model.status, you get the enum member, not the raw string. If you need the value, use model.status.value.

When nesting models, be aware that Pydantic will accept dictionaries for nested models, but it will not accept a string that looks like a dictionary. The input must be a dict or an instance of the model. Also, if you use from_attributes=True, you can initialize from ORM objects, but that is a separate configuration.

When to Split Models vs. Use Dict

For simple one-off structures, using dict as a field type may be tempting because it requires less code. However, you lose validation, type hints, and IDE support. Nested models give you explicit structure and automatic validation, but they add class definitions. The right choice depends on how much the structure is reused and how strict the validation needs to be.

If a nested structure appears in multiple models, define it as a separate model. If it is only used once and the fields are unlikely to change, a dict may be acceptable, but you will have to validate it manually. For API boundaries and configuration files, nested models are almost always the better choice because they make the schema explicit and catch errors early.

python pydantic nested models lists and enums: Practical Usa | RYUSLOG DEV