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 .