Chapter 2: declaring fields. Types, constraints, defaults, aliases. The full vocabulary.

Field

from pydantic import BaseModel, Field

class User(BaseModel):
    name: str = Field(..., min_length=1, max_length=120, description="User's full name")
    age: int = Field(default=18, ge=0, le=150)
    bio: str | None = Field(default=None, max_length=1000)

... (Ellipsis) means required. default= for default. default_factory= for callables.

Constraints

For strings:

  • min_length, max_length.
  • pattern (regex).

For numbers:

  • gt, ge, lt, le.
  • multiple_of.

For collections:

  • min_length, max_length.
class Item(BaseModel):
    sku: str = Field(..., pattern=r"^[A-Z]{3}-\d{4}$")
    quantity: int = Field(..., gt=0, multiple_of=1)
    tags: list[str] = Field(default_factory=list, max_length=20)

Annotated style

from typing import Annotated
from pydantic import StringConstraints

Username = Annotated[str, StringConstraints(min_length=3, max_length=32, pattern=r"^[a-z0-9_]+$")]

class User(BaseModel):
    username: Username

Reusable; composable.

Built-in types

Primitives

int, float, str, bool, bytes, bytearray.

Standard library

from datetime import datetime, date, time, timedelta
from decimal import Decimal
from uuid import UUID
from pathlib import Path
import enum

class M(BaseModel):
    when: datetime
    on: date
    duration: timedelta
    price: Decimal
    id: UUID
    file: Path

Native parsing for ISO 8601 dates, UUID strings, etc.

Networking

from pydantic import EmailStr, AnyUrl, HttpUrl, IPvAnyAddress

class M(BaseModel):
    email: EmailStr
    url: HttpUrl
    ip: IPvAnyAddress

EmailStr requires email-validator package.

Constrained primitives

from pydantic import conint, constr, conlist, condecimal, confloat

class M(BaseModel):
    age: conint(ge=0, le=150)
    name: constr(min_length=1, max_length=120, strip_whitespace=True)
    tags: conlist(str, max_length=10)

Older API; the Annotated style is preferred but conX still works.

Optional vs nullable

class M(BaseModel):
    a: str               # required, non-null
    b: str | None        # required, nullable
    c: str = "default"   # optional with default
    d: str | None = None # optional, nullable

Match the contract precisely.

Defaults and default_factory

class M(BaseModel):
    tags: list[str] = []                              # SHARED MUTABLE — bug
    tags2: list[str] = Field(default_factory=list)    # per-instance fresh
    created: datetime = Field(default_factory=datetime.utcnow)

Mutable defaults on Field(default=...) are shared across instances. default_factory=... creates per-instance.

Pydantic v2 will catch the mutable default and warn / error.

Aliases

When external JSON keys don’t match Python:

class User(BaseModel):
    full_name: str = Field(alias="fullName")
    
    model_config = {"populate_by_name": True}

JSON {"fullName": "Alice"}User(full_name="Alice"). With populate_by_name=True, Python names also accepted.

Validation alias vs serialization alias

class User(BaseModel):
    user_id: int = Field(validation_alias="id", serialization_alias="userId")

Different aliases for input vs output. Useful when adapting external schemas.

AliasChoices / AliasPath

For accepting multiple aliases:

from pydantic import AliasChoices

class M(BaseModel):
    name: str = Field(validation_alias=AliasChoices("name", "fullName", "full_name"))

Tries each in order.

For nested:

from pydantic import AliasPath

class M(BaseModel):
    user_id: int = Field(validation_alias=AliasPath("data", "user", "id"))

{"data": {"user": {"id": 42}}}user_id=42.

Examples in docs

class User(BaseModel):
    email: EmailStr = Field(..., examples=["[email protected]"])
    age: int = Field(..., ge=0, examples=[25, 30, 45])
    
    model_config = {
        "json_schema_extra": {
            "examples": [
                {"email": "[email protected]", "age": 30},
            ]
        }
    }

Surfaces in OpenAPI / Swagger.

Frozen

class Coord(BaseModel):
    model_config = {"frozen": True}
    
    x: float
    y: float

c = Coord(x=1, y=2)
c.x = 5  # ValidationError

Immutable. Hashable.

Strict mode

Per-model:

class M(BaseModel):
    model_config = {"strict": True}
    age: int

M.model_validate({"age": "25"})  # error

Per-field:

from pydantic import StrictInt

class M(BaseModel):
    age: StrictInt  # only int, not str

Or via Annotated:

from typing import Annotated
from pydantic import Field, Strict

age: Annotated[int, Strict()]

extra

class M(BaseModel):
    model_config = {"extra": "ignore"}    # default
    # extra: "allow" stores extras
    # extra: "forbid" errors on extras

For strict APIs: forbid catches typos.

Computed fields

from pydantic import computed_field

class User(BaseModel):
    first: str
    last: str
    
    @computed_field
    @property
    def full_name(self) -> str:
        return f"{self.first} {self.last}"

Serialized; not accepted on input.

Json type

from pydantic import Json

class M(BaseModel):
    payload: Json[dict]

Accepts a JSON string; parses; validates as dict.

Secret types

from pydantic import SecretStr

class Settings(BaseModel):
    api_key: SecretStr

s = Settings(api_key="secret")
print(s.api_key)             # SecretStr('**********')
print(s.api_key.get_secret_value())  # "secret"

Repr-safe; prevents accidental logging.

File / bytes types

from pydantic import FilePath, DirectoryPath, NewPath
from pathlib import Path

class M(BaseModel):
    config: FilePath        # must exist + be a file
    cache_dir: DirectoryPath # must exist + be a dir
    output: NewPath          # must NOT exist

For CLI tools / config validation.

Common mistakes

1. Default mutable

tags: list[str] = [] — shared. Use default_factory=list.

2. Required vs optional confusion

Mapped[str | None] doesn’t mean “optional in input.” Add = None to make it optional too.

3. Aliases without populate_by_name

Set populate_by_name=True if Python names should also work.

4. EmailStr without dependency

pip install email-validator or email-validator[idna].

5. Strict mode globally for everything

Locks down legitimate coercion (e.g., query params arrive as strings). Strict per-model where it makes sense.

What’s next

Chapter 3: Validators.

Read this next


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .