Generic models

Pydantic · Expert · 12 min

API envelopes and paginated responses repeat the same wrapper around different payloads.

A generic model has fields typed with a TypeVar. In Python 3.12+ syntax:

class Page[T](BaseModel):
    items: list[T]
    total: int

(On older versions, inherit from BaseModel, Generic[T].)

Parametrizing it, Page[User], creates a real subclass with T replaced by User, so validation, serialization and JSON Schema are fully typed for that payload. Pydantic caches parametrized classes, so Page[User] is Page[User].

Rules worth knowing:

  • An unparametrized Page validates T as Any, so items are passed through without validation. Always use the parametrized class at boundaries.
  • TypeVar bounds and defaults are respected: with T: Number (or bound=), unparametrized use validates against the bound.
  • Partial parametrization works with several TypeVars: Pair[int, T].
  • A generic model can inherit from another; parametrize the subclass with new or same TypeVars.

Each parametrization builds a new class and validator, so create them at import time (UserPage = Page[User]) rather than dynamically per request.

Generic models combine with everything else: a Page[Event] where Event is a discriminated union, or a Response[T] envelope that FastAPI uses as response_model=Response[User] to document each endpoint precisely.

Example

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str

class Page[T](BaseModel):
    items: list[T]
    total: int
    next_cursor: str | None = None

UserPage = Page[User]
p = UserPage.model_validate({"items": [{"id": "1", "name": "Ann"}], "total": 1})
print(type(p.items[0]).__name__, p.items[0].id)     # User 1
print(UserPage.__name__)                            # Page[User]
print(Page[User] is UserPage)                       # True

raw = Page.model_validate({"items": [{"id": "1"}], "total": 1})
print(raw.items)          # [{'id': '1'}]  (T unparametrized -> Any, no validation)

Remember

  • class Page[T](BaseModel) + Page[User] gives a fully typed model
  • Parametrized classes are cached and should be created at import time
  • An unparametrized generic validates its TypeVar fields as Any (or the bound)

Watch out for

  • Validating with the bare generic class and assuming the items were checked.

Check yourself

  1. What does Page.model_validate(...) do with items when Page is generic and not parametrized?

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

Official docs: docs.pydantic.dev/latest/concepts/models/#generic-models