Most business rules are simple ranges and lengths; declaring them beats writing validators by hand.
Field() attaches extra information to a field. Use it as the default value, or inside Annotated (the preferred style because the type and its rules travel together and can be reused):
price: Annotated[float, Field(gt=0)]Common arguments:
- numbers:
gt,ge,lt,le,multiple_of - strings and collections:
min_length,max_length; strings alsopattern(a regex) default=...ordefault_factory=listfor a fresh value per instance; the factory is how you getdatetime.now()at creation time instead of at importdescription,titleandexamplesflow into JSON Schema and API docsaliasfor an input name that differs from the attribute (covered later)exclude=Trueto leave a field out of dumps,repr=Falseto hide it from the repr
Constraints are enforced in the Rust core and produce specific error types, such as greater_than or string_too_short, with the limit in ctx.
Because Annotated types are just values, you can name them once and reuse them across models:
Sku = Annotated[str, Field(pattern=r"^[A-Z]{3}-\d{4}$")]A frequent bug is created: datetime = datetime.now(): the expression runs once when the class is defined, so every instance shares the same timestamp. Use Field(default_factory=datetime.now).
Example
from datetime import datetime
from typing import Annotated
from pydantic import BaseModel, Field, ValidationError
Sku = Annotated[str, Field(pattern=r"^[A-Z]{3}-\d{4}$")]
class Product(BaseModel):
sku: Sku
name: Annotated[str, Field(min_length=2, max_length=50)]
price: Annotated[float, Field(gt=0, description="Unit price in EUR")]
stock: int = Field(default=0, ge=0)
created: datetime = Field(default_factory=datetime.now)
print(Product(sku="PEN-0001", name="Pen", price=1.5).stock) # 0
try:
Product(sku="pen", name="P", price=0)
except ValidationError as e:
print([err["type"] for err in e.errors()])
# ['string_pattern_mismatch', 'string_too_short', 'greater_than']Remember
- Annotated[T, Field(...)] keeps type and constraints together and reusable
- gt/ge/lt/le, min_length/max_length and pattern cover most simple rules
- default_factory computes a fresh default per instance
Watch out for
- Using datetime.now() as a plain default, which is evaluated once at class definition.