FastAPI Pydantic Models and Data Validation - Interview Questions
1. What is Pydantic and why is it used in FastAPI?
Pydantic is a data validation library that uses Python type annotations to validate data. It’s used in FastAPI for:
- Request Validation: Automatically validates incoming request data
- Response Serialization: Converts Python objects to JSON
- Type Safety: Ensures data types match expected schemas
- Documentation: Generates OpenAPI schemas automatically
- IDE Support: Provides excellent autocomplete and type checking
from pydantic import BaseModel
class User(BaseModel):
name: str
email: str
age: int
# FastAPI automatically validates requests against this model
@app.post("/users")
def create_user(user: User):
return user
2. How do you create a basic Pydantic model?
from pydantic import BaseModel
from typing import Optional, List
class User(BaseModel):
id: int
name: str
email: str
age: Optional[int] = None
is_active: bool = True
tags: List[str] = []
# Usage
user = User(id=1, name="John", email="john@example.com")
3. What are the different field types available in Pydantic?
Pydantic supports various field types:
from pydantic import BaseModel
from typing import Optional, List, Dict, Union
from datetime import datetime, date
from decimal import Decimal
class Example(BaseModel):
# Basic types
string_field: str
int_field: int
float_field: float
bool_field: bool
# Optional fields
optional_field: Optional[str] = None
# Complex types
list_field: List[str]
dict_field: Dict[str, int]
union_field: Union[str, int]
# Date/time
datetime_field: datetime
date_field: date
# Decimal for precise numbers
decimal_field: Decimal
# Nested models
nested_field: User
4. How do you add validation to Pydantic fields?
from pydantic import BaseModel, Field, EmailStr, field_validator
from typing import Optional
class User(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
email: EmailStr
age: int = Field(..., ge=0, le=120)
password: str = Field(..., min_length=8)
@field_validator('name')
@classmethod
def name_must_be_title_case(cls, v: str) -> str:
if not v.istitle():
raise ValueError('Name must be title case')
return v
@field_validator('password')
@classmethod
def password_must_be_strong(cls, v: str) -> str:
if not any(c.isupper() for c in v):
raise ValueError('Password must contain uppercase letter')
if not any(c.isdigit() for c in v):
raise ValueError('Password must contain digit')
return v
5. What is the difference between BaseModel and BaseSettings?
BaseModel: Used for data validation and serialization
from pydantic import BaseModel
class User(BaseModel):
name: str
email: str
BaseSettings (Pydantic v2 — now in the separate pydantic-settings package): used for configuration management with environment variables.
# pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
database_url: str
api_key: str
debug: bool = False
settings = Settings()
Note: in v2, the legacy inner class Config: is replaced by model_config = ConfigDict(...) on BaseModel and model_config = SettingsConfigDict(...) on BaseSettings.
6. How do you handle nested models in Pydantic?
from pydantic import BaseModel
from typing import List, Optional
class Address(BaseModel):
street: str
city: str
country: str
postal_code: str
class User(BaseModel):
id: int
name: str
email: str
address: Address # Nested model
phone_numbers: List[str] = [] # List of strings
emergency_contact: Optional[Address] = None # Optional nested model
# Usage
user_data = {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "New York",
"country": "USA",
"postal_code": "10001"
},
"phone_numbers": ["+1234567890", "+0987654321"]
}
user = User(**user_data)
7. What are Pydantic validators and how do you use them?
Pydantic validators are methods that perform custom validation on fields:
from pydantic import BaseModel, validator
from typing import Optional
class User(BaseModel):
name: str
email: str
age: int
password: str
@validator('email')
def validate_email(cls, v):
if '@' not in v:
raise ValueError('Invalid email format')
return v.lower()
@validator('age')
def validate_age(cls, v):
if v < 0 or v > 150:
raise ValueError('Age must be between 0 and 150')
return v
@validator('password')
def validate_password(cls, v):
if len(v) < 8:
raise ValueError('Password must be at least 8 characters')
return v
@validator('name')
def validate_name(cls, v):
if not v.strip():
raise ValueError('Name cannot be empty')
return v.strip()
8. How do you use Pydantic models for request and response validation?
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class UserCreate(BaseModel):
name: str
email: str
password: str
class UserResponse(BaseModel):
id: int
name: str
email: str
is_active: bool
class Config:
orm_mode = True # For SQLAlchemy integration
class UserUpdate(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
@app.post("/users", response_model=UserResponse)
def create_user(user: UserCreate):
# user is validated against UserCreate model
# Response is validated against UserResponse model
return {"id": 1, "name": user.name, "email": user.email, "is_active": True}
@app.get("/users", response_model=List[UserResponse])
def get_users():
return [
{"id": 1, "name": "John", "email": "john@example.com", "is_active": True}
]
9. What is orm_mode in Pydantic and when do you use it?
orm_mode allows Pydantic models to work with ORM objects (like SQLAlchemy models):
from pydantic import BaseModel
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class UserModel(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
email = Column(String)
class UserResponse(BaseModel):
id: int
name: str
email: str
class Config:
orm_mode = True # Allows reading from ORM objects
# Now you can do:
# user_response = UserResponse.from_orm(db_user)
10. How do you handle optional fields and default values in Pydantic?
from pydantic import BaseModel, Field
from typing import Optional, List
class User(BaseModel):
# Required fields
name: str
email: str
# Optional fields with None as default
age: Optional[int] = None
phone: Optional[str] = None
# Fields with default values
is_active: bool = True
tags: List[str] = []
# Fields with Field() for more control
description: str = Field(default="", max_length=500)
score: float = Field(default=0.0, ge=0.0, le=100.0)
# Computed fields
@property
def display_name(self) -> str:
return f"{self.name} ({self.email})"
# Usage
user1 = User(name="John", email="john@example.com") # age=None, is_active=True
user2 = User(name="Jane", email="jane@example.com", age=25, is_active=False)
11. What are the different ways to exclude fields in Pydantic responses?
from pydantic import BaseModel, Field
from typing import Optional
class User(BaseModel):
id: int
name: str
email: str
password: str = Field(..., exclude=True) # Always excluded
secret_key: Optional[str] = Field(None, exclude=True) # Always excluded
class Config:
# Exclude unset fields
exclude_unset = True
# Exclude None values
exclude_none = True
# Exclude default values
exclude_defaults = True
# Using response_model with exclude
class UserResponse(BaseModel):
id: int
name: str
email: str
class Config:
fields = {'password': {'exclude': True}} # Alternative way
12. How do you handle custom data types in Pydantic?
from pydantic import BaseModel, validator
from datetime import datetime, date
from decimal import Decimal
from typing import Any
import re
class User(BaseModel):
# Custom string with validation
username: str
@validator('username')
def validate_username(cls, v):
if not re.match(r'^[a-zA-Z0-9_]{3,20}$', v):
raise ValueError('Username must be 3-20 characters, alphanumeric and underscore only')
return v
# Custom date handling
birth_date: date
# Custom decimal with precision
salary: Decimal = Field(..., decimal_places=2)
# Custom enum-like validation
status: str
@validator('status')
def validate_status(cls, v):
allowed_statuses = ['active', 'inactive', 'pending']
if v not in allowed_statuses:
raise ValueError(f'Status must be one of: {allowed_statuses}')
return v
Interview angle
- “Why separate request, response and database models?” - they have different fields and different trust levels. A create request has no id, a response must not expose password hashes, and the ORM model carries persistence concerns. Reusing one model everywhere is how fields leak.
- “What changed in Pydantic v2?” - the core moved to Rust, giving a large validation speedup, and the API renamed:
model_validate,model_dump,field_validator,model_config. Knowing the v1 names are deprecated matters when reading older code. - “
field_validatorormodel_validator?” - field-level for one field in isolation; model-level when the rule spans fields, such as end date after start date. Modebeforesees raw input,aftersees the parsed value. - “How do you handle a field named differently on the wire?” -
aliaspluspopulate_by_name, which keeps the external contract stable while your code uses a Pythonic name.alias_generatordoes it wholesale for camelCase APIs.