Complete Guide to Using Pydantic: Data Validation Made Easy

# Panduan Lengkap Menggunakan Pydantic: Data Validation Made Easy Pydantic adalah library Python yang powerful untuk data validation dan settings management menggunakan Python type hints. Library ini...

By Ruby Abdullah · · tutorial
PythonPydanticFastAPIData ValidationType Hints

Complete Guide to Using Pydantic: Data Validation Made Easy

Pydantic is a powerful Python library for data validation and settings management using Python type hints. It's extremely popular in the modern Python ecosystem, especially used by FastAPI, and has become the de-facto standard for data validation.

In this tutorial, we'll learn Pydantic from basics to advanced usage with practical examples.

Why Pydantic?

Pydantic Advantages:

  • Type Safety: Automatic validation based on Python type hints
  • Fast: Built with Rust for high performance (Pydantic v2)
  • Easy to Use: Intuitive and pythonic API
  • IDE Support: Autocomplete and type checking in IDE
  • JSON Schema: Automatic JSON schema generation
  • Widely Adopted: Used by FastAPI, LangChain, and many popular libraries
  • Installation

    Install Pydantic v2 (Latest)

    # Install Pydantic v2
    

    pip install pydantic

    Install with email validation

    pip install "pydantic[email]"

    Install with dotenv support

    pip install "pydantic-settings"

    Verify Installation

    import pydantic
    

    print(f"Pydantic version: {pydantic.version}")

    Should print 2.x.x

    Basic Usage: Your First Model

    1. Simple Model

    from pydantic import BaseModel
    
    

    class User(BaseModel):

    id: int

    name: str

    email: str

    age: int

    Create instance

    user = User(id=1, name="Ruby", email="ruby@rubythalib.ai", age=25)

    print(user)

    Output: id=1 name='Ruby' email='ruby@rubythalib.ai' age=25

    print(user.modeldump())

    Output: {'id': 1, 'name': 'Ruby', 'email': 'ruby@rubythalib.ai', 'age': 25}

    2. Automatic Type Conversion

    Pydantic automatically converts compatible data types:

    from pydantic import BaseModel
    
    

    class Product(BaseModel):

    id: int

    name: str

    price: float

    instock: bool

    Strings will be converted to int and float

    product = Product(

    id="123", # String -> int

    name="Laptop",

    price="15000000", # String -> float

    instock="yes" # String -> bool

    )

    print(product.id, type(product.id))

    Output: 123

    print(product.price, type(product.price))

    Output: 15000000.0

    print(product.instock, type(product.instock))

    Output: True

    3. Validation Errors

    from pydantic import BaseModel, ValidationError
    
    

    class User(BaseModel):

    id: int

    name: str

    age: int

    try:

    user = User(id="notanumber", name="Ruby", age=25)

    except ValidationError as e:

    print(e)

    # Will print detailed error about invalid id

    Output:

    1 validation error for User
    

    id

    Input should be a valid integer, unable to parse string as an integer [type=intparsing, inputvalue='notanumber', inputtype=str]

    Field Types and Constraints

    1. Optional Fields

    from typing import Optional
    

    from pydantic import BaseModel

    class User(BaseModel):

    id: int

    name: str

    email: Optional[str] = None # Optional with default None

    phone: str | None = None # Python 3.10+ syntax

    user1 = User(id=1, name="Ruby")

    print(user1.email) # None

    user2 = User(id=2, name="Ahmad", email="ahmad@example.com")

    print(user2.email) # ahmad@example.com

    2. Default Values

    from pydantic import BaseModel
    
    

    class Product(BaseModel):

    id: int

    name: str

    price: float = 0.0

    category: str = "General"

    instock: bool = True

    product = Product(id=1, name="Notebook")

    print(product.modeldump())

    {'id': 1, 'name': 'Notebook', 'price': 0.0, 'category': 'General', 'instock': True}

    3. Field with Constraints

    from pydantic import BaseModel, Field
    
    

    class User(BaseModel):

    id: int = Field(gt=0, description="User ID must be positive")

    name: str = Field(minlength=1, maxlength=100)

    email: str = Field(pattern=r'^[\w\.-]+@[\w\.-]+\.\w+

    )

    age: int = Field(ge=0, le=150, description="Age must be between 0 and 150")

    score: float = Field(ge=0.0, le=100.0)

    Valid

    user = User(

    id=1,

    name="Ruby Abdullah",

    email="ruby@rubythalib.ai",

    age=25,

    score=95.5

    )

    Invalid - will raise ValidationError

    try:

    invaliduser = User(

    id=-1, # Error: must be > 0

    name="", # Error: minlength=1

    email="invalid-email", # Error: pattern doesn't match

    age=200, # Error: must be <= 150

    score=101.0 # Error: must be <= 100

    )

    except ValidationError as e:

    print(e)

    Available Field Constraints:

    4. Common Field Types

    from datetime import datetime, date, time
    

    from typing import List, Dict, Set, Tuple

    from pydantic import BaseModel, EmailStr, HttpUrl, FilePath, DirectoryPath

    class AdvancedModel(BaseModel):

    # String types

    email: EmailStr # Requires: pip install "pydantic[email]"

    website: HttpUrl

    # Numeric types

    integer: int

    floating: float

    # Boolean

    isactive: bool

    # Datetime types

    createdat: datetime

    birthdate: date

    logintime: time

    # Collections

    tags: List[str]

    metadata: Dict[str, str]

    uniqueids: Set[int]

    coordinates: Tuple[float, float]

    # File paths (will validate existence)

    # configfile: FilePath

    # datadir: DirectoryPath

    Example usage

    data = AdvancedModel(

    email="ruby@rubythalib.ai",

    website="https://rubythalib.ai",

    integer=42,

    floating=3.14,

    isactive=True,

    createdat="2024-01-15T10:30:00",

    birthdate="1990-05-20",

    logintime="14:30:00",

    tags=["python", "pydantic", "tutorial"],

    metadata={"author": "Ruby", "version": "1.0"},

    uniqueids={1, 2, 3, 4, 5},

    coordinates=(106.8456, -6.2088)

    )

    print(data.email) # ruby@rubythalib.ai

    print(type(data.createdat)) #

    Custom Validators

    1. Field Validator

    from pydantic import BaseModel, fieldvalidator
    
    

    class User(BaseModel):

    username: str

    email: str

    age: int

    @fieldvalidator('username')

    @classmethod

    def usernamealphanumeric(cls, v: str) -> str:

    if not v.isalnum():

    raise ValueError('Username must be alphanumeric')

    return v.lower() # Convert to lowercase

    @fieldvalidator('email')

    @classmethod

    def emailmusthaveat(cls, v: str) -> str:

    if '@' not in v:

    raise ValueError('Invalid email address')

    return v

    @fieldvalidator('age')

    @classmethod

    def agemustbeadult(cls, v: int) -> int:

    if v < 18:

    raise ValueError('User must be at least 18 years old')

    return v

    Valid

    user = User(username="Ruby123", email="ruby@example.com", age=25)

    print(user.username) # ruby123 (converted to lowercase)

    Invalid

    try:

    invalid = User(username="ruby@#$", email="ruby@example.com", age=15)

    except ValidationError as e:

    print(e)

    2. Multiple Field Validator

    from pydantic import BaseModel, fieldvalidator
    
    

    class Product(BaseModel):

    name: str

    price: float

    discountprice: float

    @fieldvalidator('name', 'price', 'discountprice')

    @classmethod

    def checknotempty(cls, v):

    if isinstance(v, str) and not v.strip():

    raise ValueError('Field cannot be empty')

    if isinstance(v, (int, float)) and v < 0:

    raise ValueError('Value cannot be negative')

    return v

    3. Model Validator (Cross-Field Validation)

    from pydantic import BaseModel, modelvalidator
    
    

    class DateRange(BaseModel):

    startdate: datetime

    enddate: datetime

    @modelvalidator(mode='after')

    def checkdates(self):

    if self.enddate <= self.startdate:

    raise ValueError('enddate must be after startdate')

    return self

    class Product(BaseModel):

    name: str

    price: float

    discountprice: float

    @modelvalidator(mode='after')

    def checkdiscountprice(self):

    if self.discountprice >= self.price:

    raise ValueError('discountprice must be less than price')

    return self

    Valid

    product = Product(name="Laptop", price=10000000, discountprice=8500000)

    Invalid

    try:

    invalid = Product(name="Laptop", price=10000000, discountprice=11000000)

    except ValidationError as e:

    print(e)

    Nested Models

    1. Simple Nested Model

    from typing import List
    

    from pydantic import BaseModel

    class Address(BaseModel):

    street: str

    city: str

    country: str

    postalcode: str

    class User(BaseModel):

    id: int

    name: str

    email: str

    address: Address # Nested model

    Create instance

    user = User(

    id=1,

    name="Ruby Abdullah",

    email="ruby@rubythalib.ai",

    address={

    "street": "Mojo 007/007",

    "city": "Surakarta",

    "country": "Indonesia",

    "postalcode": "57118"

    }

    )

    print(user.address.city) # Surakarta

    print(user.modeldump())

    2. List of Nested Models

    from typing import List
    

    from pydantic import BaseModel

    class OrderItem(BaseModel):

    productid: int

    productname: str

    quantity: int

    price: float

    @property

    def total(self) -> float:

    return self.quantity * self.price

    class Order(BaseModel):

    orderid: int

    customername: str

    items: List[OrderItem] # List of nested models

    @property

    def totalamount(self) -> float:

    return sum(item.total for item in self.items)

    Create order

    order = Order(

    orderid=1001,

    customername="Ruby Abdullah",

    items=[

    {"productid": 1, "productname": "Laptop", "quantity": 1, "price": 15000000},

    {"productid": 2, "productname": "Mouse", "quantity": 2, "price": 150000},

    {"productid": 3, "productname": "Keyboard", "quantity": 1, "price": 500000}

    ]

    )

    print(f"Total items: {len(order.items)}")

    print(f"Total amount: Rp {order.totalamount:,.0f}")

    for item in order.items:

    print(f"{item.productname}: Rp {item.total:,.0f}")

    Model Configuration

    1. Model Config

    from pydantic import BaseModel, ConfigDict
    
    

    class User(BaseModel):

    modelconfig = ConfigDict(

    strstripwhitespace=True, # Strip whitespace from strings

    validateassignment=True, # Validate on assignment

    frozen=False, # Allow modification (True = immutable)

    extra='forbid' # Forbid extra fields

    )

    id: int

    name: str

    email: str

    Whitespace will be stripped

    user = User(id=1, name=" Ruby ", email=" ruby@example.com ")

    print(f"'{user.name}'") # 'Ruby' (no whitespace)

    Validate on assignment (because validateassignment=True)

    user.name = "Ahmad" # Valid

    print(user.name) # Ahmad

    Extra fields forbidden (because extra='forbid')

    try:

    invalid = User(id=1, name="Ruby", email="ruby@example.com", age=25)

    except ValidationError as e:

    print(e) # Error: Extra inputs are not permitted

    2. Field Aliases

    from pydantic import BaseModel, Field
    
    

    class User(BaseModel):

    id: int

    fullname: str = Field(alias='fullName') # Different JSON key

    emailaddress: str = Field(alias='emailAddress')

    Parse from JSON with camelCase

    userdata = {

    'id': 1,

    'fullName': 'Ruby Abdullah',

    'emailAddress': 'ruby@rubythalib.ai'

    }

    user = User(userdata)

    print(user.fullname) # Ruby Abdullah

    Export with alias

    print(user.modeldump(byalias=True))

    {'id': 1, 'fullName': 'Ruby Abdullah', 'emailAddress': 'ruby@rubythalib.ai'}

    Integration with FastAPI

    Pydantic is a core dependency of FastAPI for request/response validation.

    1. Request Body Validation

    from fastapi import FastAPI
    

    from pydantic import BaseModel, EmailStr, Field

    app = FastAPI()

    class UserCreate(BaseModel):

    username: str = Field(minlength=3, maxlength=50)

    email: EmailStr

    password: str = Field(minlength=8)

    age: int = Field(ge=18, le=120)

    class UserResponse(BaseModel):

    id: int

    username: str

    email: str

    age: int

    @app.post("/users/", responsemodel=UserResponse)

    async def createuser(user: UserCreate):

    # Pydantic automatically validates the request body

    # If validation fails, FastAPI returns 422 error

    # Simulate user creation

    userdata = user.modeldump()

    userdata['id'] = 1

    # Password excluded from response (not in UserResponse model)

    return UserResponse(userdata)

    Settings Management with Pydantic Settings

    1. Environment Variables

    from pydanticsettings import BaseSettings
    

    from pydantic import Field

    class Settings(BaseSettings):

    # Database

    databaseurl: str = Field(alias='DATABASEURL')

    databasepoolsize: int = 5

    # API

    apikey: str = Field(alias='APIKEY')

    apitimeout: int = 30

    # App

    appname: str = "My Application"

    debug: bool = False

    class Config:

    envfile = '.env'

    envfileencoding = 'utf-8'

    Load from .env file and environment variables

    settings = Settings()

    print(settings.databaseurl)

    print(settings.appname)

    Best Practices

    1. Use Type Hints Correctly

    from typing import List, Dict, Optional
    

    from pydantic import BaseModel

    ❌ Bad

    class User(BaseModel):

    tags: list # Not specific

    metadata: dict # Not specific

    ✅ Good

    class User(BaseModel):

    tags: List[str] # Specific type

    metadata: Dict[str, str] # Specific key-value types

    2. Use Field for Documentation

    from pydantic import BaseModel, Field
    
    

    class Product(BaseModel):

    id: int = Field(description="Unique product identifier")

    name: str = Field(minlength=1, maxlength=200, description="Product name")

    price: float = Field(gt=0, description="Product price in IDR")

    Auto-generate JSON schema with descriptions

    print(Product.modeljsonschema())

    3. Separate Request and Response Models

    from pydantic import BaseModel
    

    from typing import Optional

    from datetime import datetime

    Request model (for create)

    class UserCreate(BaseModel):

    username: str

    email: str

    password: str

    Request model (for update)

    class UserUpdate(BaseModel):

    username: Optional[str] = None

    email: Optional[str] = None

    password: Optional[str] = None

    Response model (doesn't include password)

    class UserResponse(BaseModel):

    id: int

    username: str

    email: str

    createdat: datetime

    updatedat: datetime

    Common Pitfalls and Solutions

    1. Mutable Default Values

    from pydantic import BaseModel
    

    from typing import List

    ❌ Bad - Mutable default

    class User(BaseModel):

    tags: List[str] = [] # Dangerous!

    ✅ Good - Use defaultfactory

    from pydantic import Field

    class User(BaseModel):

    tags: List[str] = Field(defaultfactory=list)

    2. Dict vs Model

    from pydantic import BaseModel
    
    

    class User(BaseModel):

    name: str

    age: int

    user = User(name="Ruby", age=25)

    ❌ Bad - Dict access will error

    print(user['name']) # KeyError

    ✅ Good - Access as attribute

    print(user.name) # Ruby

    ✅ Good - Convert to dict if needed

    userdict = user.modeldump()

    print(userdict['name']) # Ruby

    Conclusion

    Pydantic is a powerful tool for:

    Key Takeaways:

  • Data Validation: Automatic type validation with Python type hints
  • Easy to Use: Intuitive and pythonic API
  • Performance: Fast validation with Pydantic v2 (Rust-powered)
  • Integration: Perfect for FastAPI, CLI tools, config management
  • Maintainability: Type hints help with IDE autocomplete and type checking
  • When to Use Pydantic:

    ✅ Use Pydantic when:

    ❌ Don't use Pydantic when:

    Resources:

    Happy coding with Pydantic! 🚀

    Related Articles

    Complete Instructor Tutorial: Structured Outputs from LLMs with Pydantic

    Tutorial Lengkap Instructor: Output Terstruktur dari LLM dengan Pydantic Halo temen-temen, di tutorial ini aku mau ngaja...

    Mirascope Tutorial: A Pythonic Toolkit for Building LLM Applications

    Tutorial Mirascope: Toolkit Pythonic untuk Membangun Aplikasi LLM Pendahuluan Mirascope adalah library Python yang menye...

    LitServe Tutorial: Fast and Easy AI Model Serving Framework

    Tutorial LitServe: Framework Serving Model AI yang Cepat dan Mudah Pendahuluan LitServe adalah framework open-source dar...

    Reflex Tutorial: Building Full-Stack Web Apps in Pure Python

    Reflex: Membangun Aplikasi Web Full-Stack dengan Python Murni Reflex memungkinkan Anda membangun aplikasi web lengkap — ...