A Simple Guide to Pydantic in Python

Dipak MaliDipak Mali
5 min read

Pydantic is a powerful Python library for data validation and settings management using type annotations. It simplifies parsing, validating, and serializing data, making it a go-to choice for building robust APIs, configuration management, and data processing pipelines. Below, we explore key Pydantic features with concise explanations and examples using a Patient model for a healthcare application.

Key Pydantic Features

1. Type-Based Validation with BaseModel

Pydantic's BaseModel class enables data validation using Python type hints. It ensures that input data conforms to the defined types and constraints, raising clear errors for invalid data.

Example: The Patient class uses type hints to enforce data types like str, int, float, and Optional fields.

class Patient(BaseModel):
    name: str
    age: int = Field(..., gt=0, le=120)
    height: float = Field(..., gt=0, le=3)

2. Field Constraints with Field

The Field function allows you to add constraints like minimum/maximum values, lengths, and default values to model fields. It also supports metadata like descriptions.

age: int = Field(..., gt=0, le=120, description="Patient's Age")
height: float = Field(..., gt=0, le=3, description="Height in meters")

3. Nested Models

Pydantic supports nested models, allowing complex data structures like objects within objects. This is useful for representing relationships, such as an Address within a Patient.

Example: The Address model is nested within the Patient model to store location details.

class Address(BaseModel):
    road: str = Field(..., min_length=3)
    city: str = Field(..., min_length=2)

class Patient(BaseModel):
    address: Optional[Address] = None

4. Custom Validators with field_validator

The @field_validator decorator allows custom validation logic for specific fields. You can validate data beyond type checking, such as ensuring an email has a valid domain.

Example: The check_email_domain validator ensures the email domain is from a predefined list.

@field_validator("email", mode="after")
@classmethod
def check_email_domain(cls, value):
    valid_domains = ["axix.com", "hdfc.com", "kotak.com"]
    if value:
        domain = value.split("@")[-1]
        if domain not in valid_domains:
            raise ValueError(f"Invalid email domain. Allowed: {valid_domains}")
    return value

5. Model-Wide Validation with model_validator

The @model_validator decorator enables validation across multiple fields in a model. This is useful for enforcing business rules that depend on multiple attributes.

Example: The check_contact_info validator ensures at least one contact method (email or phone) is provided.

@model_validator(mode="after")
def check_contact_info(self):
    if not self.email and not self.contact:
        raise ValueError("Either email or contact must be provided.")
    return self

6. Computed Fields with computed_field

The @computed_field decorator allows you to define properties that are dynamically calculated but included in the model's serialized output.

Example: The bmi field computes the patient's Body Mass Index based on weight and height.

@computed_field
@property
def bmi(self) -> float:
    return round(self.weight / (self.height ** 2), 2)

7. Alias Support for Field Names

Pydantic allows defining aliases for field names using the Field function's alias parameter. This is useful when the input data (e.g., JSON) uses different keys than the model.

Example: The id__ field uses an alias id to handle external data with a key named id.

id__: str = Field(default_factory=lambda: str(uuid4()), alias="id")

8. Serialization with model_dump

Pydantic provides methods like model_dump to serialize models into dictionaries, with options to include aliases or exclude unset fields. This is ideal for API responses or data storage.

Example: Serializing a Patient object to a dictionary with aliases.

print(patient_obj.model_dump(by_alias=True))

9. Support for Special Types

Pydantic supports special types like EmailStr, AnyUrl, and Literal for stricter validation of emails, URLs, and specific string literals.

Example: The email field uses EmailStr for email validation, and gender uses Literal to restrict values.

email: Optional[EmailStr] = Field(default=None, max_length=50)
gender: Literal["male", "female", "other"]

Example Usage

Below is a complete example demonstrating these features in a Patient model.

from uuid import uuid4
from typing import List, Dict, Optional, Literal
from datetime import date, datetime
from pydantic import BaseModel, Field, EmailStr, AnyUrl, field_validator, model_validator, computed_field

class Address(BaseModel):
    houseno: Optional[int] = Field(default=None, ge=1, description="House Number if available")
    road: str = Field(..., min_length=3)
    city: str = Field(..., min_length=2)
    pincode: Optional[str] = Field(default=None, pattern=r"^\d{6}$", description="6-digit pincode")

    def __str__(self):
        return f"{self.houseno or ''}, {self.road}, {self.city} - {self.pincode or ''}"

class Patient(BaseModel):
    """Represents a patient's basic health and contact information."""
    id__: str = Field(default_factory=lambda: str(uuid4()), alias="id")
    name: str = Field(..., min_length=2, max_length=50, description="Patient's full name")
    gender: Literal["male", "female", "other"]
    age: int = Field(..., gt=0, le=120, description="Patient's Age")
    dob: Optional[date] = Field(default=None, description="Date of birth")
    height: float = Field(..., gt=0, le=3, description="Height in meters")
    weight: float = Field(..., gt=0, le=500, description="Weight in kilograms")
    email: Optional[EmailStr] = Field(default=None, max_length=50, description="Valid corporate email")
    contact: Optional[Dict[str, str]] = Field(default=None, description="Emergency contact details")
    address: Optional[Address] = None
    allergies: Optional[List[str]] = Field(default_factory=list, description="Known allergies")
    doc_url: Optional[AnyUrl] = Field(default=None, description="Patient medical document link")
    registered_on: datetime = Field(default_factory=datetime.utcnow)

    @computed_field
    @property
    def bmi(self) -> float:
        return round(self.weight / (self.height ** 2), 2)

    @field_validator("email", mode="after")
    @classmethod
    def check_email_domain(cls, value):
        valid_domains = ["axix.com", "hdfc.com", "kotak.com"]
        if value:
            domain = value.split("@")[-1]
            if domain not in valid_domains:
                raise ValueError(f"Invalid email domain. Allowed: {valid_domains}")
        return value

    @model_validator(mode="after")
    def check_contact_info(self):
        if not self.email and not self.contact:
            raise ValueError("Either email or contact must be provided.")
        return self

# Demo Usage
p1_address = {
    "road": "Prabhat Road",
    "city": "Pune",
    "pincode": "411004"
}

p1_data = {
    "name": "Shivam",
    "gender": "male",
    "age": 23,
    "height": 1.75,
    "weight": 70.5,
    "email": "hello@axix.com",
    "contact": {"phone": "1234567890"},
    "address": p1_address,
    "allergies": ["Dust", "Weat"],
    "doc_url": "https://hospital.com/doc.pdf"
}

patient_obj = Patient(**p1_data)

# Access
print("City:", patient_obj.address.city)
print("BMI:", patient_obj.bmi)
print("Patient ID:", patient_obj.id__)

# Dump to dict
print("\nSerialized Patient Data:")
print(patient_obj.model_dump(by_alias=True))

Why Use Pydantic?

Pydantic simplifies data validation, reduces boilerplate code, and ensures type safety. Its integration with Python's type hints makes it intuitive, while features like custom validators and computed fields provide flexibility for complex use cases. Whether you're building APIs, managing configurations, or processing structured data, Pydantic is a reliable choice.

0
Subscribe to my newsletter

Read articles from Dipak Mali directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dipak Mali
Dipak Mali

Hello, I'm Dipak, a Junior Software Engineer with a strong foundation in React JS, Python, and Java. I’m a working professional focused on developing efficient software solutions in the dynamic adtech industry. Beyond core coding, I specialize in RESTful API design, web development, and have hands-on experience with technologies like Spring Boot, MySQL, Docker, and AWS cloud services. I thrive on creating scalable, user-centric applications and enjoy tackling complex problem-solving challenges. Eager to collaborate, I’m passionate about innovation, continuous learning, and making a meaningful impact through technology.