Skip to main content

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-validator package).
  • HttpUrl, AnyUrl: Validates URLs; AnyUrl accepts 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 as int; dict[str, int] validates keys as str, values as int.

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_length for lists, sets, dicts.
  • Use default_factory=list or default_factory=dict for mutable defaults.
  • Discriminated unions with Union[Type1, Type2] and discriminator="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.

Further Reading