Pydantic v2 Guide: Modern Data Validation in Python

Pydantic Data Validation Python Code Schema Parsing Rust Core

Receiving unvalidated JSON payloads across public HTTP endpoints or parsing untyped database rows causes runtime type errors, subtle data corruption, and security vulnerabilities.

Pydantic v2 rewrites Python's data validation ecosystem on top of a high-performance compiled Rust engine (pydantic-core), offering 5x to 20x higher throughput, strict static type safety, and ergonomic model validation hooks.


Step 1: Environment Setup & Virtual Environment

Pydantic v2 requires Python 3.8+ and comes with precompiled Rust wheels for Windows, Linux, and macOS.

Create an isolated virtual environment and install the required dependencies:

# Create virtual environment
python -m venv pydantic-env

# Activate virtual environment
# Windows (PowerShell): .\pydantic-env\Scripts\Activate.ps1
# Linux / macOS: source pydantic-env/bin/activate

# Install project dependencies
pip install "pydantic>=2.0" pydantic-settings email-validator

Step 2: Architecture & Pydantic v1 vs. v2 Core Architecture

In Pydantic v2, data validation and serialization execute directly in compiled Rust (pydantic-core), bypassing slow interpreted Python loops.

Field validation is separated into @field_validator for single fields and @model_validator for cross-field validation.

Pydantic v1 vs. Pydantic v2 Upgrades

FeaturePydantic v1 (Deprecated)Pydantic v2 (Modern Standard)Performance Impact
Validation CoreInterpreted Python loopsCompiled Rust engine (pydantic-core)5x to 20x faster validation speed
Dictionary Exportmodel.dict()model.model_dump()Direct zero-copy memory extraction
JSON Serializationmodel.json()model.model_dump_json()Direct Rust SIMD JSON serializer
Field Validation@validator@field_validatorStrict static type checker compliance
Cross-Field Logic@root_validator@model_validator(mode='after')Full instance validation lifecycle hooks

Step 3: Step-by-Step Schema Implementation Breakdown (Component Breakdown)

Step 3.1: Defining Data Types and Field Constraints

Use Field() to attach metadata, minimum/maximum boundaries, regex patterns, and default values to individual schema properties:

from pydantic import BaseModel, Field, EmailStr, field_validator, model_validator
from datetime import datetime
from typing import Optional

class UserRegistrationSchema(BaseModel):
    username: str = Field(..., min_length=3, max_length=30, description="Alphanumeric username")
    email: EmailStr
    password: str = Field(..., min_length=8)
    confirm_password: str = Field(..., min_length=8)
    age: int = Field(..., ge=18, le=120)
    registered_at: datetime = Field(default_factory=datetime.utcnow)

Step 3.2: Implementing Custom Field Normalization

Use @field_validator with @classmethod to sanitize or normalize values (such as converting usernames to lowercase and checking alphanumeric characters):

    # Custom Single-Field Validator
    @field_validator("username")
    @classmethod
    def validate_and_normalize_username(cls, value: str) -> str:
        if not value.isalnum():
            raise ValueError("Username must contain only alphanumeric characters without spaces or symbols.")
        return value.lower()

Step 3.3: Cross-Field Validation with Model Validator

Use @model_validator(mode='after') to inspect multiple fields simultaneously after their individual types have been validated:

    # Cross-Field Model Validator
    @model_validator(mode="after")
    def verify_password_match(self) -> 'UserRegistrationSchema':
        if self.password != self.confirm_password:
            raise ValueError("Password confirmation does not match the provided password.")
        return self

Step 4: How to Run and Verify Output

Execute the application from your terminal:

python test_schema.py

Expected terminal output verification:

Validated User Object:
{
  "username": "alexdev2026",
  "email": "alex@ilabacademy.com",
  "age": 29,
  "registered_at": "2026-08-25T00:30:00.000000Z"
}

Step 5: Complete, Working Final Code

Here is the complete, consolidated implementation ready for production testing. Save this script as main.py:

from pydantic import BaseModel, Field, EmailStr, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from datetime import datetime
from typing import Optional

# 1. User Validation Schema
class UserRegistrationSchema(BaseModel):
    username: str = Field(..., min_length=3, max_length=30, description="Alphanumeric username")
    email: EmailStr
    password: str = Field(..., min_length=8)
    confirm_password: str = Field(..., min_length=8)
    age: int = Field(..., ge=18, le=120)
    registered_at: datetime = Field(default_factory=datetime.utcnow)

    @field_validator("username")
    @classmethod
    def validate_and_normalize_username(cls, value: str) -> str:
        if not value.isalnum():
            raise ValueError("Username must contain only alphanumeric characters.")
        return value.lower()

    @model_validator(mode="after")
    def verify_password_match(self) -> 'UserRegistrationSchema':
        if self.password != self.confirm_password:
            raise ValueError("Password confirmation does not match password.")
        return self

# 2. Production Settings Configuration Schema
class AppSettings(BaseSettings):
    app_name: str = "iLab Academy API"
    environment: str = Field(default="production", pattern="^(development|staging|production)$")
    database_url: str = Field(..., min_length=15)
    max_connections: int = Field(default=20, ge=1, le=100)

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        case_sensitive=False
    )

if __name__ == "__main__":
    payload = {
        "username": "AlexDev2026",
        "email": "alex@ilabacademy.com",
        "password": "SuperSecretPassword123!",
        "confirm_password": "SuperSecretPassword123!",
        "age": 29
    }
    
    user = UserRegistrationSchema(**payload)
    print("Validated User Object:")
    print(user.model_dump_json(indent=2, exclude={"password", "confirm_password"}))

Step 6: Production Hardening, Edge Cases & Security

  • BaseSettings Moved Package: In Pydantic v2, BaseSettings is no longer in the core library. Install pydantic-settings via pip install pydantic-settings.
  • Fast JSON Serialization: Prefer model.model_dump_json() over json.dumps(model.model_dump()) to leverage Rust SIMD JSON serialization.
  • Frozen Immutable Models: Use model_config = ConfigDict(frozen=True) to make models hashable and thread-safe.

Step 7: Frequently Asked Questions & Troubleshooting

Q: What is the difference between mode='before' and mode='after' in @model_validator?
A: mode='before' receives raw input data as a Python dict before Pydantic performs any type coercion. mode='after' receives the fully validated model instance.

Q: How do I exclude sensitive fields from serialization?
A: Pass exclude={'password'} into model.model_dump() or model.model_dump_json().

Post a Comment

0 Comments