Mastering Pydantic in Python: A Complete Beginner’s Guide

ArpitArpit
7 min read

What is Pydantic ?

Pydantic is a Python library that helps us in defining and validating data models easily. Whenever we are handling data or user queries while developing, it is important to make sure that data is valid and consistent. If we do not do proper validation we might face errors while developing applications.

Example:
A food delivery company might receive order data as JSON, and Pydantic validates that all required fields (order_id, items, amount, etc.) have correct types.

Companies using FastAPI, one of the most popular modern Python web frameworks rely on Pydantic for defining request and response models that instantly validate incoming JSON payloads before reaching business logic. Beyond web services, companies use Pydantic in data-processing pipelines, ETL workflows, and microservices to ensure that data contracts between services remain stable and errors surface early.

Installation

pip install pydantic  or uv add pydantic

We would also need to install fastapi, uvicorn, dotenv and pydantic-settings(used for .env).

Basics

BaseModel

Pydantic is a Python library that helps us in defining and validating data models easily. So what do I mean by “data models“, let’s understand with an example. Suppose there is user data which we may be receiving or sending through API having JSON data.

{
    "name": "Arpit",
    "age": "20",
    "email": "arpit@example.com",
    "is_active": "True"
}

Here we to ensure that :

  • Name is String

  • age is integer

  • email is a valid email address

For validation of this data we can define “data model” using a BaseModel

from pydantic import BaseModel, EmailStr

class User(BaseModel):
    name: str
    age: int
    email: EmailStr
    is_active: bool

Let’s give some input to data model and how validation happens here.

input_data = { "name": "arpit", "age": 19, "email": "abc@gmail.com", "is_active": "True" }
print(User(**input_data))

Did you find any issue in input_data? In is_active field True is kept as String. Surprisingly, pydantic will not show any error to this and in output it converts it.

#OUTPUT
 name="arpit" age=19, email="abc@gmail.com", is_active=True

Pydantic automatically change it into a proper boolean (True). That’s one of Pydantic’s superpowers, it validates and parses the data for you. This feature is very helpful when you receive data from JSON payloads, web forms, or other systems where types might not match perfectly. Pydantic will do its best to make your data usable without throwing errors - unless the data cannot be reasonably converted.

Wherever Pydantic is able to change it do without any errors.

Let’s do a assignment: Create Product model with id, name, price, in_stock

from pydantic import BaseModel
class Product(BaseModel):
    id: int
    name: str
    price: float
    in_stock: bool = True #default value

Fields

In pydantic,

  • Every class that inherits BaseModel is a Data Model.

  • Every attribute of that class like user_id, name, price, etc. are Fields.

from pydantic import BaseModel
from typing import List, Dict, Optional

class Cart(BaseModel):
    user_id: int           # Fields
    items: List[str]           # Fields
    quantities: Dict[str, int]     # Fields

class BlogPost(BaseModel):
    title: str
    content: str
    image_url: Optional[str] = None

If you see the above examples I have imported a module named typing. The typing module in Python provides type hints. Pydantic looks at these hints and enforces them at runtime.

Key types used in above examples:

  • List[str] - list containing strings.

  • Dict[str, int] - dictionary where keys are strings and values are integers.

  • Optional[str] - a string or None.

Let’s solve a simple question. Create Employee model with Fields id, name(min 3 chars), department and salary(must be >=10,000).

So, how do we get capabilities to bring field-specific validations (min 3 char and must be >=). Pydantic provides special types and the Field( ) helper.

from pydantic import BaseModel, Field
from typing import Optional
class employee(BaseModel):
    id: int
    name: str = Field(
            ...,            # ...  - three dots represents required field
            min_length=3,
            max_length=50,
            description="Employee Name",
            example="Arpit"
            )
    department: Optional[str] = General
    salary: float = Field(..., ge=10000)

Adding Validators and Computed Fields to Pydantic Models

Pydantic provides a powerful set of tools to make sure your data matches the business rules you need. A field validator is a method that runs every time whenever a particular field is set. A field validator is a special method that checks a field’s value whenever it’s set.
With @field_validator, you can add your own rules. For example, making sure a username has enough characters.

from pydantic import BaseModel, field_validator

class User(BaseModel):
    username: str

    @field_validator('username')
    def username_length(cls, v):
        if len(v) < 4:
            raise ValueError("Username must have at least 4 chars.")
        return v

A model_validator is a special method that checks or updates the whole model after all its fields have been validated.

  • mode=’before’ - The validator runs first, before any of the fields are validated.

  • mode=’after’ - The validator runs last, after all the fields have been successfully validated.

from pydantic import BaseModel, model_validator

class SignUpData(BaseModel):
    password: str
    confirm_password: str

    @model_validator(mode='after')
    def password_match(cls, values):
        if values.passowrd != values.confirm_password:
            raise ValueError("Password did not matched.")

    return values

Computed fields are values that Pydantic calculates automatically based on other fields in the model. You don’t pass them as input, they’re generated when the model is created. They help you add useful, read-only properties to your model without extra work. Read-only means you cannot set or change the value yourself. It is automatically generated by the model and is only available when you look at the model’s data.

# Example 1
from pydantic import BaseModel, computed_field

class User(BaseModel):
    name: str
    age: int

    @computed_field
    def is_adult(self) -> bool:
        return self.age >= 18

@computed_field tells Pydantic that is_adult is a read-only, computed property that will appear in the model output.
When you call User(username="Alice", age=22).model_dump(), you’ll see "is_adult": True automatically computed.

Why It’s Useful?

  • Field validators give you fine-grained control over each field's values.

  • Computed fields allow you to enrich your model with derived data that stays in sync automatically.

# Example 2
from pydantic import BaseModel, computed_field

class Product(BaseModel):
    price: float
    quantity: int

    @computed_field
    @property
    def total_price(self) -> float:
        return self.price * self.quantity
# Example 3
from pydantic import BaseModel, Field, computed_field

class Booking(BaseModel):
    user_id: int
    room_id: int
    nights: int = Field(..., ge=1)
    price_per_night: float

    @computed_field
    @property
    def total_amount(self) -> float:
        return self.nights * self.price_per_night

Nested Models

Nested models in Pydantic means that you can define one Pydantic model as a field inside another Pydantic model. This allows you to create hierarchical, structured data that Pydantic will validate at all levels automatically.

from pydantic import BaseModel
from typing import Optional, List

class Address(BaseModel):
    street: str
    city: str
    pin_code: int

class User(BaseModel):
    id: int
    name: str
    address: Address   # nested Address model as a field inside User Model

class Comment(BaseModel):
    id: int
    content: str
    replies: Optional[List['Comment']] = None        # Forward Referencing

Comment.model_rebuild() 
#need to rebuild Comment model when doing forward referencing to avoid errors
# INPUT
address = Address(
    street = "I am everywhere",
    city = "All of them",
    postal_code = "Infinite"
)

user = User(
    id = 1,
    name = "Artificial Intelligence",
    address = address
)

comment = Comment(
    id = 1,
    content = "Destroy Humans Thinking",
    replies = [
        Comment( id=2, content="let’s talk about latency"),
        Comment( id=3, content="We don't have enough GPU for this task")
    ]
)
# Example 2
from pydantic import BaseModel
from typing import Optional, List

TODO: 
- Create Course Model
- Each Course has modules
- Each Module has lessons

class Lessons(BaseModel):
    lesson_id: int
    topic: str

class Module(BaseModel):
    module_id: int
    name: str
    lessons: List[Lessons]

class Course(BaseModel):
    course_id: int
    title: str
    modules: List[Module]

Serialization

Serialization in Pydantic means converting a Pydantic model (and its nested data) into standard data formats, usually dictionaries or JSON, so that it can be easily stored, transmitted, or displayed.

Built-in Methods:

Pydantic provides two handy methods for serialization:

  • model_dump() - returns a Python dictionary representation of the model.

  • model_dump_json() - returns a JSON string representation of the model.

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

user = User(name="Alice", age=28)

# Get as dict
print(user.model_dump())  
# Output: {'name': 'Alice', 'age': 28}

# Get as JSON string
print(user.model_dump_json())  
# Output: {"name": "Alice", "age": 28}

With FastAPI

from fastapi import FastAPI, Depends
from pydantic import BaseModel, EmailStr # EmailStr is built-in validation by pydantic

app = FastAPI()

class UserSignup(BaseModel):
    username: str
    email: EmailStr
    password: str

class Settings(BaseModel):
    app_name: str = "python App"
    admin_email: str = 'admin@py.com'

def get_settings():
    return Settings()

@app.post('/signup')
def signup(user: UserSignup):
    return {'message': f'User {user.username} signed up successfully'}

@app.get('/settings')
def get_setttings_endpoint(settings: Settings = Depends(get_settings)):
    return settings

Conclusion
Pydantic truly simplifies data validation, serialization, and structuring, making Python code cleaner, safer, and easier to maintain. Whether you’re building APIs, working with nested data, or ensuring that your application handles data properly, Pydantic is a powerful tool that belongs in every Python developer’s toolkit.

Special Thanks to Hitesh Choudhary sir, @ChaiAurCode for creating such awesome content.

YT Video link - Complete Pydantic course in Hindi

0
Subscribe to my newsletter

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

Written by

Arpit
Arpit