A Simple Guide to Pydantic in Python


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.
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.