Pydantic Field Validation: Types and Constraints
Pydantic's true power lies in its field types and constraint system. Instead of writing validator functions, you declare constraints directly in the field definition using the Field() function. Pydantic includes dozens of built-in validators—for email addresses, URLs, file paths, numeric ranges, string patterns—all without custom code. This article covers the most common field types and constraints you'll use daily.
Built-In Field Types
Beyond Python's basic types (str, int, float), Pydantic recognizes domain-specific types that bring validation semantics:
from pydantic import BaseModel, EmailStr, HttpUrl, FilePath
from uuid import UUID
from datetime import datetime
class User(BaseModel):
id: UUID # Must be a valid UUID
email: EmailStr # Must be a valid email
website: HttpUrl # Must be a valid HTTP/HTTPS URL
created_at: datetime # Parses ISO 8601 strings
profile_pic: FilePath # Must be an existing file path
# Valid instantiation
user = User(
id="550e8400-e29b-41d4-a716-446655440000",
email="[email protected]",
website="https://example.com",
created_at="2026-06-02T10:30:00Z",
profile_pic="/home/user/avatar.png"
)
# Invalid email raises ValidationError
try:
bad = User(
id="550e8400-e29b-41d4-a716-446655440000",
email="not-an-email", # Missing @ symbol
website="https://example.com",
created_at="2026-06-02T10:30:00Z",
profile_pic="/home/user/avatar.png"
)
except Exception as e:
print("Email validation failed")
Common specialized types include:
- EmailStr: Validates email format (requires the
email-validatorpackage). - HttpUrl, AnyUrl: Validates URLs;
AnyUrlaccepts ftp://, file://, etc. - FilePath, DirectoryPath: Confirms the path exists on the filesystem.
- UUID: Validates UUID format (v1-v5).
- datetime, date, time: Parses ISO 8601 strings; stores as Python objects.
- Decimal, UUID: High-precision numbers and unique identifiers.
String Constraints
The Field() function lets you add constraints without writing validators. For strings:
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(min_length=1, max_length=100)
description: str = Field(min_length=10)
sku: str = Field(pattern=r"^[A-Z0-9]{6,10}$") # 6-10 alphanumeric
category: str = Field(min_length=1)
# Valid
product = Product(
name="Laptop",
description="High-performance computing device",
sku="ABC123D",
category="Electronics"
)
# Invalid: name too long
try:
bad = Product(
name="X" * 150, # Exceeds max_length=100
description="Valid description",
sku="ABC123D",
category="Electronics"
)
except Exception as e:
print("Validation failed: name exceeds 100 chars")
# Invalid: SKU doesn't match pattern
try:
bad2 = Product(
name="Laptop",
description="Valid description",
sku="lowercase", # Doesn't match pattern
category="Electronics"
)
except Exception as e:
print("Validation failed: SKU must be 6-10 alphanumeric uppercase")
Common string constraints:
- min_length, max_length: String length bounds.
- pattern: Regex pattern the string must match (as a string regex, not compiled).
- regex: Alternative spelling for
pattern.
Numeric Constraints
For integers and floats, control the range:
from pydantic import BaseModel, Field
class Rating(BaseModel):
score: int = Field(ge=1, le=5) # Greater-or-equal, less-or-equal
percentage: float = Field(ge=0.0, le=100.0)
count: int = Field(gt=0) # Strict greater-than
discount: float = Field(ge=0.0, lt=1.0) # Less-than (exclusive)
# Valid
rating = Rating(score=4, percentage=85.5, count=1000, discount=0.15)
# Invalid: score out of range
try:
bad = Rating(score=10, percentage=85.5, count=1000, discount=0.15)
except Exception as e:
print("score must be between 1 and 5")
Constraint symbols:
- ge: Greater than or equal (
>=). - gt: Greater than (
>). - le: Less than or equal (
<=). - lt: Less than (
<). - multiple_of: Value must be a multiple of the specified number.
Collections and Structured Types
Pydantic validates container types recursively:
from pydantic import BaseModel, Field
from typing import list, dict
class Library(BaseModel):
book_ids: list[int] = Field(min_length=1) # Non-empty list of ints
tags: list[str] = Field(max_length=10) # At most 10 tags
metadata: dict[str, str] # Key-value pairs
# Valid
lib = Library(
book_ids=[1, 2, 3],
tags=["fiction", "classic"],
metadata={"author": "Unknown", "year": "1900"}
)
# Invalid: empty list violates min_length
try:
bad = Library(
book_ids=[], # Violates min_length=1
tags=["fiction"],
metadata={"author": "Unknown"}
)
except Exception as e:
print("book_ids must contain at least one item")
# List items are validated per type
lib2 = Library(
book_ids=["1", "2"], # Strings coerced to ints
tags=["sci-fi", "cyberpunk"],
metadata={}
)
print(lib2.book_ids) # [1, 2]
Container constraints:
- min_length, max_length: Number of items in the list/set/dict.
- min_items, max_items: Aliases for lists.
- Nested types:
list[int]validates each item asint;dict[str, int]validates keys asstr, values asint.
Discriminated Unions and Enums
For fields that can be one of several types, use discriminated unions:
from pydantic import BaseModel, Field
from typing import Union, Literal
class SuccessResponse(BaseModel):
status: Literal["success"]
data: str
class ErrorResponse(BaseModel):
status: Literal["error"]
error_code: int
class Response(BaseModel):
response: Union[SuccessResponse, ErrorResponse] = Field(
discriminator="status"
)
# Valid: success response
resp = Response(
response={"status": "success", "data": "OK"}
)
print(resp.response.data) # "OK"
# Valid: error response
resp2 = Response(
response={"status": "error", "error_code": 500}
)
print(resp2.response.error_code) # 500
The discriminator="status" tells Pydantic to use the status field to determine which union member to instantiate. This is much faster than trying each variant.
Default Values and Factory Functions
For complex defaults, use a factory function:
from pydantic import BaseModel, Field
from typing import list
class Task(BaseModel):
title: str
tags: list[str] = Field(default_factory=list) # New list per instance
priority: int = 1
metadata: dict = Field(default_factory=dict)
task1 = Task(title="Build API")
task2 = Task(title="Write tests")
# Each gets its own list/dict (not shared)
task1.tags.append("urgent")
print(task2.tags) # [] (not affected by task1's append)
Always use default_factory=list or default_factory=dict for mutable defaults. Never use default=[] or default={} in Pydantic models.
Validation Example: Complete User Schema
Here's a realistic schema combining multiple field types and constraints:
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
from typing import Optional
class Address(BaseModel):
street: str = Field(min_length=1)
city: str = Field(min_length=1)
zip_code: str = Field(pattern=r"^\d{5}$") # US ZIP code
class UserProfile(BaseModel):
username: str = Field(
min_length=3, max_length=20,
pattern=r"^[a-zA-Z0-9_]+$" # Alphanumeric + underscore
)
email: EmailStr
age: int = Field(ge=13, le=150)
bio: Optional[str] = Field(max_length=500, default=None)
address: Optional[Address] = None
joined_at: datetime = Field(default_factory=datetime.now)
# Valid
user = UserProfile(
username="alice_wonder",
email="[email protected]",
age=28,
bio="Python enthusiast",
address={
"street": "123 Main St",
"city": "Portland",
"zip_code": "97201"
}
)
# Invalid: username too short, age out of range
try:
bad = UserProfile(
username="ab", # Too short
email="[email protected]",
age=10 # Too young
)
except Exception as e:
print("Validation errors:", e)
Key Takeaways
- Pydantic includes specialized types:
EmailStr,HttpUrl,UUID,datetime,FilePath. - Use
Field()to add string constraints:min_length,max_length,pattern. - Numeric constraints:
ge(>=),gt(>),le(<=),lt(<),multiple_of. - Container constraints:
min_length,max_lengthfor lists, sets, dicts. - Use
default_factory=listordefault_factory=dictfor mutable defaults. - Discriminated unions with
Union[Type1, Type2]anddiscriminator="field"handle polymorphic data efficiently.
Frequently Asked Questions
What if I need a custom type Pydantic doesn't recognize?
Use a custom validator (covered in the next article) or a Annotated type with custom metadata. For example, Annotated[str, Field(pattern="...")] allows you to attach validation to any type.
How do I make a field case-insensitive for enum-like strings?
Use Literal with lowercase strings and configure the model to lowercase input: set str_to_lower=True in ConfigDict. Or write a validator that calls .lower() before validation.
Can I apply multiple constraints to one field?
Yes. Combine constraints in Field(): Field(min_length=5, max_length=20, pattern=r"^[a-z]+$"). All constraints are applied in order.
What's the difference between List[int] and list[int]?
In Python 3.9+, use list[int] (built-in generic). For Python 3.8, use List[int] from typing. Pydantic accepts both.
How do I validate a field only if another field has a certain value?
Use field validators or model validators (covered in the next article). Conditional validation requires custom logic.