Panduan Lengkap Menggunakan 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

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 sangat populer di ekosistem Python modern, terutama digunakan oleh FastAPI, dan menjadi standar de-facto untuk validasi data.

Dalam tutorial ini, kita akan mempelajari Pydantic dari dasar hingga advanced usage dengan contoh praktis.

Mengapa Pydantic?

Keunggulan Pydantic:

  • Type Safety: Validasi otomatis berdasarkan type hints Python
  • Fast: Built dengan Rust untuk performa tinggi (Pydantic v2)
  • Easy to Use: API yang intuitif dan pythonic
  • IDE Support: Autocomplete dan type checking di IDE
  • JSON Schema: Automatic JSON schema generation
  • Widely Adopted: Digunakan FastAPI, LangChain, dan banyak library populer
  • Instalasi

    Install Pydantic v2 (Latest)

    # Install Pydantic v2
    

    pip install pydantic

    Install dengan email validation

    pip install "pydantic[email]"

    Install dengan dotenv support

    pip install "pydantic-settings"

    Verifikasi Instalasi

    import pydantic
    

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

    Should print 2.x.x

    Basic Usage: Model Pertama Anda

    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 otomatis convert tipe data yang compatible:

    from pydantic import BaseModel
    
    

    class Product(BaseModel):

    id: int

    name: str

    price: float

    instock: bool

    String akan di-convert ke int dan 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)

    # Akan print error detail tentang 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 dan Constraints

    1. Optional Fields

    from typing import Optional
    

    from pydantic import BaseModel

    class User(BaseModel):

    id: int

    name: str

    email: Optional[str] = None # Optional dengan 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 dengan 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 - akan raise ValidationError

    try:

    invaliduser = User(

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

    name="", # Error: minlength=1

    email="invalid-email", # Error: pattern tidak match

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

    score=101.0 # Error: must be <= 100

    )

    except ValidationError as e:

    print(e)

    Field Constraints yang Tersedia:

    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}")

    3. Deep Nesting

    from typing import List
    

    from pydantic import BaseModel

    class Skill(BaseModel):

    name: str

    level: int # 1-10

    class Education(BaseModel):

    degree: str

    institution: str

    year: int

    class Experience(BaseModel):

    company: str

    position: str

    years: int

    skillsused: List[Skill]

    class Employee(BaseModel):

    id: int

    name: str

    education: List[Education]

    experience: List[Experience]

    Complex nested data

    employee = Employee(

    id=1,

    name="Ruby Abdullah",

    education=[

    {

    "degree": "Bachelor of Computer Science",

    "institution": "University of Indonesia",

    "year": 2020

    }

    ],

    experience=[

    {

    "company": "PT Rubythalib Data Konsulta",

    "position": "Data Scientist",

    "years": 3,

    "skillsused": [

    {"name": "Python", "level": 9},

    {"name": "Machine Learning", "level": 8},

    {"name": "FastAPI", "level": 9}

    ]

    }

    ]

    )

    Access deep nested data

    print(employee.experience[0].skillsused[0].name) # Python

    Model Configuration

    1. Model Config

    from pydantic import BaseModel, ConfigDict
    
    

    class User(BaseModel):

    modelconfig = ConfigDict(

    strstripwhitespace=True, # Strip whitespace dari string

    validateassignment=True, # Validate saat assignment

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

    extra='forbid' # Forbid extra fields

    )

    id: int

    name: str

    email: str

    Whitespace akan di-strip

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

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

    Validate on assignment (karena validateassignment=True)

    user.name = "Ahmad" # Valid

    print(user.name) # Ahmad

    Extra fields forbidden (karena 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. Alias untuk Field Names

    from pydantic import BaseModel, Field
    
    

    class User(BaseModel):

    id: int

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

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

    Parse dari JSON dengan camelCase

    userdata = {

    'id': 1,

    'fullName': 'Ruby Abdullah',

    'emailAddress': 'ruby@rubythalib.ai'

    }

    user = User(userdata)

    print(user.fullname) # Ruby Abdullah

    Export dengan alias

    print(user.modeldump(byalias=True))

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

    3. Custom Serialization

    from datetime import datetime
    

    from pydantic import BaseModel, fieldserializer

    class Event(BaseModel):

    name: str

    timestamp: datetime

    @fieldserializer('timestamp')

    def serializetimestamp(self, value: datetime) -> str:

    return value.strftime('%Y-%m-%d %H:%M:%S')

    event = Event(name="Meeting", timestamp=datetime.now())

    print(event.modeldump())

    {'name': 'Meeting', 'timestamp': '2024-01-15 10:30:00'}

    Parsing dan Serialization

    1. Parse dari JSON

    from pydantic import BaseModel
    
    

    class User(BaseModel):

    id: int

    name: str

    email: str

    From JSON string

    jsonstr = '{"id": 1, "name": "Ruby", "email": "ruby@example.com"}'

    user = User.modelvalidatejson(jsonstr)

    print(user)

    From dict

    data = {"id": 2, "name": "Ahmad", "email": "ahmad@example.com"}

    user2 = User(data)

    print(user2)

    2. Export ke JSON

    from pydantic import BaseModel
    

    from typing import List

    class User(BaseModel):

    id: int

    name: str

    tags: List[str]

    user = User(id=1, name="Ruby", tags=["python", "data-science"])

    To dict

    print(user.modeldump())

    {'id': 1, 'name': 'Ruby', 'tags': ['python', 'data-science']}

    To JSON string

    print(user.modeldumpjson())

    '{"id":1,"name":"Ruby","tags":["python","data-science"]}'

    To JSON string with indent

    print(user.modeldumpjson(indent=2))

    3. Exclude Fields

    from pydantic import BaseModel
    
    

    class User(BaseModel):

    id: int

    name: str

    email: str

    password: str

    isadmin: bool

    user = User(

    id=1,

    name="Ruby",

    email="ruby@example.com",

    password="secret123",

    isadmin=True

    )

    Exclude password dari export

    print(user.modeldump(exclude={'password'}))

    {'id': 1, 'name': 'Ruby', 'email': 'ruby@example.com', 'isadmin': True}

    Include hanya field tertentu

    print(user.modeldump(include={'id', 'name', 'email'}))

    {'id': 1, 'name': 'Ruby', 'email': 'ruby@example.com'}

    Integration dengan FastAPI

    Pydantic adalah core dependency FastAPI untuk 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)

    2. Query Parameters Validation

    from fastapi import FastAPI, Query
    

    from pydantic import BaseModel, Field

    from typing import Optional

    app = FastAPI()

    class SearchParams(BaseModel):

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

    page: int = Field(default=1, ge=1)

    size: int = Field(default=10, ge=1, le=100)

    sort: Optional[str] = None

    @app.get("/search/")

    async def search(params: SearchParams = Query()):

    return {

    "query": params.q,

    "page": params.page,

    "size": params.size,

    "results": [] # Simulate search results

    }

    Settings Management dengan 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)

    .env file:

    DATABASEURL=postgresql://user:pass@localhost/db
    

    APIKEY=your-secret-key

    DEBUG=true

    2. Multiple Environments

    from pydanticsettings import BaseSettings
    

    from typing import Literal

    class Settings(BaseSettings):

    environment: Literal['development', 'staging', 'production'] = 'development'

    databaseurl: str

    redisurl: str

    debug: bool = False

    class Config:

    envfile = '.env'

    settings = Settings()

    if settings.environment == 'production':

    # Production configuration

    pass

    elif settings.environment == 'development':

    # Development configuration

    pass

    Best Practices

    1. Gunakan Type Hints dengan Benar

    from typing import List, Dict, Optional
    

    from pydantic import BaseModel

    ❌ Bad

    class User(BaseModel):

    tags: list # Tidak specific

    metadata: dict # Tidak specific

    ✅ Good

    class User(BaseModel):

    tags: List[str] # Specific type

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

    2. Gunakan Field untuk Dokumentasi

    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. Pisahkan Request dan Response Models

    from pydantic import BaseModel
    

    from typing import Optional

    from datetime import datetime

    Request model (untuk create)

    class UserCreate(BaseModel):

    username: str

    email: str

    password: str

    Request model (untuk update)

    class UserUpdate(BaseModel):

    username: Optional[str] = None

    email: Optional[str] = None

    password: Optional[str] = None

    Response model (tidak include password)

    class UserResponse(BaseModel):

    id: int

    username: str

    email: str

    createdat: datetime

    updatedat: datetime

    4. Use Validators untuk Business Logic

    from pydantic import BaseModel, fieldvalidator
    

    import re

    class Order(BaseModel):

    ordernumber: str

    totalamount: float

    @fieldvalidator('ordernumber')

    @classmethod

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

    # Business rule: Order number format ORD-YYYY-NNNN

    pattern = r'^ORD-\d{4}-\d{4}

    if not re.match(pattern, v):

    raise ValueError(

    'Order number must follow format ORD-YYYY-NNNN'

    )

    return v

    @fieldvalidator('totalamount')

    @classmethod

    def validateamount(cls, v: float) -> float:

    # Business rule: Minimum order 10,000

    if v < 10000:

    raise ValueError('Minimum order amount is 10,000')

    return v

    Common Pitfalls dan Solutions

    1. Mutable Default Values

    from pydantic import BaseModel
    

    from typing import List

    ❌ Bad - Mutable default

    class User(BaseModel):

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

    ✅ Good - Use defaultfactory

    from pydantic import Field

    class User(BaseModel):

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

    2. Validation Order

    from pydantic import BaseModel, fieldvalidator
    
    

    class User(BaseModel):

    email: str

    username: str

    # Validator dijalankan sesuai urutan definisi field

    @fieldvalidator('username')

    @classmethod

    def usernamebasedonemail(cls, v, info):

    # info.data berisi field yang sudah divalidasi sebelumnya

    if 'email' in info.data:

    # Email sudah tersedia karena didefinisikan lebih dulu

    pass

    return v

    3. Dict vs Model

    from pydantic import BaseModel
    
    

    class User(BaseModel):

    name: str

    age: int

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

    ❌ Bad - Akses sebagai dict akan error

    print(user['name']) # KeyError

    ✅ Good - Akses sebagai attribute

    print(user.name) # Ruby

    ✅ Good - Convert to dict jika perlu

    userdict = user.modeldump()

    print(userdict['name']) # Ruby

    Advanced: Generic Models

    from typing import Generic, TypeVar, List
    

    from pydantic import BaseModel

    T = TypeVar('T')

    class ResponseModel(BaseModel, Generic[T]):

    success: bool

    message: str

    data: T

    class User(BaseModel):

    id: int

    name: str

    class Product(BaseModel):

    id: int

    name: str

    price: float

    Use generic model

    userresponse = ResponseModel

    data=User(id=1, name="Ruby"">User

    )

    productresponse = ResponseModel[List[Product]](

    success=True,

    message="Products retrieved successfully",

    data=[

    Product(id=1, name="Laptop", price=15000000),

    Product(id=2, name="Mouse", price=150000)

    ]

    )

    print(userresponse.data.name) # Ruby

    print(productresponse.data[0].name) # Laptop

    Kesimpulan

    Pydantic adalah tool yang sangat powerful untuk:

    Key Takeaways:

  • Data Validation: Automatic type validation dengan Python type hints
  • Easy to Use: API yang intuitif dan pythonic
  • Performance: Fast validation dengan Pydantic v2 (Rust-powered)
  • Integration: Perfect untuk FastAPI, CLI tools, config management
  • Maintainability: Type hints membantu IDE autocomplete dan type checking
  • Kapan Menggunakan Pydantic:

    ✅ Use Pydantic when:

    ❌ Don't use Pydantic when:*

    Resources:

    Selamat menggunakan Pydantic! 🚀

    Artikel Terkait

    Tutorial Logfire: Platform Observability untuk Aplikasi Python dan AI

    Tutorial Logfire: Platform Observability untuk Aplikasi Python dan AI Logfire adalah platform observability buatan tim P...

    Tutorial Lengkap Instructor: Output Terstruktur dari LLM dengan Pydantic

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

    Tutorial Mirascope: Toolkit Pythonic untuk Membangun Aplikasi LLM

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

    Tutorial LitServe: Framework Serving Model AI yang Cepat dan Mudah

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