backend / web frameworks / fastapi / 02_pydantic_models.md

FastAPI Pydantic Models and Data Validation - Interview Questions

4 interview angles 6 min read source

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_validator or model_validator?” - field-level for one field in isolation; model-level when the rule spans fields, such as end date after start date. Mode before sees raw input, after sees the parsed value.
  • “How do you handle a field named differently on the wire?” - alias plus populate_by_name, which keeps the external contract stable while your code uses a Pythonic name. alias_generator does it wholesale for camelCase APIs.