Pydantic is a data validation and settings management library for Python. It uses Python type annotations to validate data and enforce type hints at runtime, making your code more robust, readable, and maintainable.
Consider this common scenario: your Python application receives data from an API, a database, or a user input form. Before processing this data, you need to ensure it has the correct structure and values. Without proper validation, you might encounter:
- Runtime errors: When code tries to access missing fields or apply operations to values of the wrong type
- Data inconsistency: Processing invalid data leading to corruption or incorrect results
- Security vulnerabilities: Accepting malformed data that could exploit your application
Traditional approaches to validation in Python involve writing extensive if/else statements to check data types, ranges, formats, and other constraints. This leads to code that is:
- Verbose and repetitive
- Error-prone
- Hard to maintain
- Difficult to document
Pydantic solves these problems by leveraging Python's type hints to:
- Automatically validate data against the specified types and constraints
- Convert input data to the appropriate Python types when possible
- Generate clear error messages when validation fails
- Create self-documenting models through type annotations
At its core, Pydantic works by defining models as classes that inherit from BaseModel. These models use Python type annotations to specify the expected data types and constraints for each field.
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
is_active: bool = True # Field with a default value
# Valid data - will pass validation
valid_user = User(id=1, name="John Doe", email="john@example.com")
print(valid_user)
# Output: id=1 name='John Doe' email='john@example.com' is_active=True
# Invalid data - will raise a validation error
try:
invalid_user = User(id="not an integer", name=123, email="not an email")
except Exception as e:
print(f"Validation error: {e}")
# Will show detailed validation errors for each fieldWhen you create a Pydantic model instance, Pydantic:
- Checks if all required fields are present
- Validates that each value matches its expected type
- Attempts to convert values to the right type when possible
- Applies any additional validation rules
- Returns a validated object or raises a
ValidationError
Pydantic not only validates but also tries to convert data to the appropriate type:
from pydantic import BaseModel
class Item(BaseModel):
id: int
price: float
quantity: int
# Strings will be converted to numbers if possible
item = Item(id="42", price="9.99", quantity="5")
print(item)
# Output: id=42 price=9.99 quantity=5
print(type(item.id)) # <class 'int'>Pydantic supports complex types from Python's typing module:
from typing import List, Dict, Optional
from pydantic import BaseModel
class Order(BaseModel):
id: int
items: List[str]
metadata: Dict[str, str] = {}
notes: Optional[str] = None
# All these are valid
order1 = Order(id=1, items=["apple", "orange"])
order2 = Order(id=2, items=["book"], metadata={"source": "online"})
order3 = Order(id=3, items=["laptop"], notes="Express delivery")Models can be nested within other models:
from pydantic import BaseModel
class Address(BaseModel):
street: str
city: str
zip_code: str
class User(BaseModel):
name: str
address: Address
# Create a user with a nested address
user = User(
name="Jane Smith",
address={
"street": "123 Main St",
"city": "Anytown",
"zip_code": "12345"
}
)
print(user.address.city) # Output: AnytownPydantic allows adding constraints to fields:
from pydantic import BaseModel, Field, EmailStr
class User(BaseModel):
name: str = Field(..., min_length=2, max_length=50)
age: int = Field(..., ge=0, lt=120) # greater than or equal to 0, less than 120
email: EmailStr # Special email validator (requires email-validator package)
password: str = Field(..., min_length=8)
# This will fail validation
try:
user = User(name="J", age=150, email="not-an-email", password="short")
except Exception as e:
print(f"Validation errors: {e}")You can add custom validation logic:
from pydantic import BaseModel, validator
class SignupRequest(BaseModel):
username: str
password1: str
password2: str
@validator('username')
def username_alphanumeric(cls, v):
if not v.isalnum():
raise ValueError('must be alphanumeric')
return v
@validator('password2')
def passwords_match(cls, v, values):
if 'password1' in values and v != values['password1']:
raise ValueError('passwords do not match')
return v
# This will fail custom validation
try:
signup = SignupRequest(
username="user@name", # Not alphanumeric
password1="secret123",
password2="different" # Doesn't match password1
)
except Exception as e:
print(f"Validation errors: {e}")Pydantic is commonly used with FastAPI to validate incoming requests and outgoing responses:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
is_offer: bool = False
@app.post("/items/")
def create_item(item: Item):
# FastAPI automatically validates the request body using the Item model
return {"item_name": item.name, "processed": True}Pydantic can be used to define and validate application settings:
from pydantic import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
debug: bool = False
database_url: str
api_keys: list[str] = []
class Config:
env_file = ".env" # Load settings from .env file
# Settings will be loaded from environment variables or .env file
settings = Settings()Pydantic is excellent for parsing and normalizing data from external sources:
import json
from pydantic import BaseModel
class Product(BaseModel):
id: int
name: str
price: float
tags: list[str] = []
# Parse JSON data
json_data = '{"id": 123, "name": "Laptop", "price": 999.99, "tags": ["electronics", "computer"]}'
product = Product.parse_raw(json_data)
# Convert to dictionary
product_dict = product.model_dump()- Reduced Boilerplate: Less code needed for validation and error handling
- Improved Code Quality: Clearer intentions and self-documenting models
- Better Error Messages: Helpful validation errors that pinpoint issues
- Type Safety: Runtime type checking that complements static type checking
- IDE Support: Better code completion and type hinting in modern IDEs
- Documentation: Automatic schema generation for OpenAPI docs
| Feature | Pydantic | dataclasses | attrs | marshmallow |
|---|---|---|---|---|
| Type Validation | ✅ | ❌ (runtime) | ❌ (runtime) | ✅ |
| Type Conversion | ✅ | ❌ | ❌ | ✅ |
| Schema Generation | ✅ | ❌ | ❌ | ✅ |
| Performance | Very Fast | Fast | Fast | Slower |
| Integration with FastAPI | Native | Limited | Limited | Limited |
Pydantic is particularly valuable when:
- Working with external data sources (APIs, files, databases)
- Building web applications with FastAPI
- Creating data-heavy applications that need validation
- You want runtime type checking to complement static type checking
- You need to validate complex nested data structures
- You want automatic documentation generation
Let's look at a practical example of validating a contact form submission:
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr, Field, validator
class ContactForm(BaseModel):
name: str = Field(..., min_length=2, max_length=100)
email: EmailStr
subject: str = Field(..., min_length=5, max_length=200)
message: str = Field(..., min_length=10)
phone: Optional[str] = None
submit_date: datetime = Field(default_factory=datetime.now)
@validator('phone')
def validate_phone(cls, v):
if v is None:
return v
# Remove all non-digit characters
digits_only = ''.join(filter(str.isdigit, v))
# Check if we have a valid number of digits
if len(digits_only) < 10 or len(digits_only) > 15:
raise ValueError('Phone number must have between 10 and 15 digits')
# Format the phone number
return digits_only
class Config:
# Example of adding schema extra info for documentation
schema_extra = {
"example": {
"name": "John Doe",
"email": "john@example.com",
"subject": "Inquiry about your services",
"message": "I would like to learn more about the services you offer.",
"phone": "+1 (555) 123-4567"
}
}
# Test with valid data
valid_submission = ContactForm(
name="Jane Smith",
email="jane@example.com",
subject="Question about pricing",
message="I would like to know more about your pricing options.",
phone="(555) 987-6543"
)
print(valid_submission)
print(f"Cleaned phone: {valid_submission.phone}") # Output: 5559876543
# Test with invalid data
try:
invalid_submission = ContactForm(
name="J", # Too short
email="not-an-email", # Invalid email
subject="Hi", # Too short
message="Short", # Too short
phone="123" # Too few digits
)
except Exception as e:
print(f"Validation errors: {e}")Understanding how to handle validation errors and the most common types of failures is crucial for effectively using Pydantic in production applications.
When Pydantic validation fails, it raises a ValidationError exception. Here are several strategies for handling these errors:
The most straightforward approach is using try/except blocks:
from pydantic import BaseModel, ValidationError
class User(BaseModel):
username: str
age: int
try:
user = User(username="john", age="not an integer")
except ValidationError as e:
print(f"Validation failed: {e}")
# Handle the error appropriatelyThe ValidationError contains detailed information about what went wrong:
from pydantic import BaseModel, ValidationError
class User(BaseModel):
username: str
age: int
try:
user = User(username=123, age="invalid")
except ValidationError as e:
# Get JSON representation of errors
error_json = e.json()
print(error_json)
# Access individual errors
for error in e.errors():
print(f"Error location: {error['loc']}")
print(f"Error type: {error['type']}")
print(f"Error message: {error['msg']}")For web applications, you can create custom error handlers:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import ValidationError
app = FastAPI()
@app.exception_handler(ValidationError)
async def validation_exception_handler(request: Request, exc: ValidationError):
return JSONResponse(
status_code=422,
content={
"detail": exc.errors(),
"message": "Invalid input data. Please check your request body."
}
)Use validators to provide fallbacks instead of rejecting the entire model:
from pydantic import BaseModel, validator
class Settings(BaseModel):
port: int = 8000
@validator('port')
def ensure_valid_port(cls, v):
try:
port = int(v)
if 1 <= port <= 65535:
return port
return 8000 # Default fallback
except (ValueError, TypeError):
return 8000 # Default fallbackThese are the most frequent validation issues you'll encounter:
The most common errors relate to incorrect data types:
from pydantic import BaseModel
class Item(BaseModel):
id: int
price: float
name: str
# These will all fail with type errors
Item(id="abc", price="12.99", name=123)The error would indicate that:
- 'abc' is not a valid integer
- '12.99' would actually be converted to a float
- 123 would be converted to a string
When required fields are not provided:
from pydantic import BaseModel
class User(BaseModel):
username: str
email: str
# This will fail due to missing required field 'email'
User(username="john")When values don't meet the defined constraints:
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(..., min_length=3)
price: float = Field(..., gt=0)
# These will fail constraint validation
Product(name="AB", price=0) # name too short, price must be > 0Errors raised by custom validators:
from pydantic import BaseModel, validator
class Password(BaseModel):
value: str
@validator('value')
def strong_password(cls, v):
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain at least one digit")
return v
# This will fail custom validation
Password(value="weakpwd")When Pydantic can't convert a value to the desired type:
from pydantic import BaseModel
from datetime import datetime
class Event(BaseModel):
timestamp: datetime
# This will fail as "invalid-date" can't be parsed as a datetime
Event(timestamp="invalid-date")-
Be Specific with Error Messages: Provide clear error messages in custom validators
-
Layer Validation: Use multi-level validation for complex requirements
-
Log Validation Errors: Log detailed validation errors for debugging
-
User-Friendly Error Messages: Transform technical validation errors into user-friendly messages
-
Graceful Degradation: When appropriate, use default values instead of raising errors
-
Centralized Error Handling: In web applications, handle validation errors consistently
-
Security Considerations: Avoid leaking sensitive information in error messages
Example of transforming Pydantic errors into user-friendly messages:
from pydantic import BaseModel, ValidationError, EmailStr
class SignupForm(BaseModel):
username: str
email: EmailStr
password: str
def user_friendly_errors(validation_error: ValidationError):
friendly_errors = []
error_map = {
"value_error.email": "Please enter a valid email address",
"value_error.missing": "This field is required",
"type_error.string": "This field must be text",
# Add more mappings as needed
}
for error in validation_error.errors():
error_type = f"{error['type']}"
field = error['loc'][0] if error['loc'] else "unknown field"
if error_type in error_map:
friendly_errors.append(f"{field.capitalize()}: {error_map[error_type]}")
else:
friendly_errors.append(f"{field.capitalize()}: {error['msg']}")
return friendly_errors
try:
form = SignupForm(username="", email="not-an-email", password=123)
except ValidationError as e:
friendly_messages = user_friendly_errors(e)
for message in friendly_messages:
print(message)Understanding these common errors and how to handle them efficiently will help you build more robust applications with Pydantic.
Pydantic has revolutionized data validation in Python by making it more concise, safer, and easier to maintain. By leveraging Python's type annotations, it provides a powerful yet intuitive way to ensure data correctness at runtime.
Whether you're building APIs, processing external data, or just want to make your code more robust, Pydantic offers a modern solution to the age-old problem of data validation in Python applications.
With proper error handling strategies in place, you can ensure your applications gracefully handle invalid data while providing meaningful feedback to users.
Start using Pydantic today to write more reliable code with less effort!