Last modified: Oct 24, 2026

Data Validation with Pydantic v2

Bad data breaks good code. Every API request, config file, and database row can carry wrong types or missing fields.

Pydantic v2 solves this problem. It checks your data against a schema and raises clear errors when something is wrong.

In this guide, you will learn how to build models, validate input, and handle errors. Every example uses Pydantic v2 syntax.

Why Use Pydantic v2?

Pydantic is a data validation library for Python. You describe the shape of your data with type hints, and Pydantic enforces it.

Version 2 is a full rewrite. Its core is written in Rust, so it runs much faster than v1.

It also adds stronger type checking, better error messages, and a cleaner API. Many frameworks now rely on it, so learning v2 pays off.

Installing Pydantic v2

Install the package with pip. Make sure you get version 2 or newer.


pip install "pydantic>=2"

Check the installed version to confirm the setup.


import pydantic

print(pydantic.VERSION)

2.7.1

Your First Pydantic Model

A model is a class that inherits from BaseModel. Each field is a typed class attribute.

When you create an instance, Pydantic checks every value against its type.


from pydantic import BaseModel

class User(BaseModel):
    id: int          # must be an integer
    name: str        # must be a string
    active: bool     # must be a boolean

# Valid data
user = User(id=1, name="Alice", active=True)
print(user)

id=1 name='Alice' active=True

Now pass a bad value. Pydantic rejects it and explains why.


# Invalid data: id is not a number
user = User(id="abc", name="Alice", active=True)

pydantic_core._pydantic_core.ValidationError: 1 validation error for User
id
  Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]

The error names the field, the problem, and the bad input. That makes debugging fast.

Automatic Type Coercion

Pydantic is lenient by default. It converts compatible values, such as the string "42" into an integer.


user = User(id="42", name="Bob", active="true")
print(user.id, type(user.id))

42 

This is handy for form data and JSON. But sometimes you want strict types with no conversion.

Strict Mode

Strict mode disables coercion. A string stays a string, and it will not pass as an integer.


from pydantic import BaseModel, ConfigDict

class StrictUser(BaseModel):
    model_config = ConfigDict(strict=True)
    id: int
    name: str

# This now fails, because "42" is a string
StrictUser(id="42", name="Bob")

pydantic_core._pydantic_core.ValidationError: 1 validation error for StrictUser
id
  Input should be a valid integer [type=int_type, input_value='42', input_type=str]

Use strict mode when exact types matter, like in financial or security code.

Field Constraints

Types alone are not enough. You often need rules like minimum length or value ranges.

Use Field to add constraints and defaults to a field.


from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str = Field(min_length=3, max_length=50)
    price: float = Field(gt=0)          # must be greater than 0
    quantity: int = Field(default=0, ge=0)  # default and >= 0

p = Product(name="Lamp", price=19.99)
print(p)

name='Lamp' price=19.99 quantity=0

Now try an invalid price. The rule is enforced.


Product(name="Lamp", price=-5)

pydantic_core._pydantic_core.ValidationError: 1 validation error for Product
price
  Input should be greater than 0 [type=greater_than, input_value=-5, input_type=int]

Nested Models

Real data is often nested. Pydantic validates nested models automatically.


from pydantic import BaseModel

class Address(BaseModel):
    city: str
    zip_code: str

class Customer(BaseModel):
    name: str
    address: Address     # nested model

c = Customer(name="Ann", address={"city": "Paris", "zip_code": "75001"})
print(c.address.city)

Paris

Pydantic converts the plain dict into an Address instance. If a nested field is wrong, the error points to the exact path.

Custom Validators

Some rules cannot be expressed with types. Use field_validator to write your own logic.


from pydantic import BaseModel, field_validator

class Account(BaseModel):
    username: str

    @field_validator("username")
    @classmethod
    def check_username(cls, value: str) -> str:
        if not value.isalnum():
            raise ValueError("username must be alphanumeric")
        return value.lower()

a = Account(username="Alice123")
print(a.username)

alice123

Validators can also clean data. Here the username is lowercased before it is stored.

This pattern is useful when you send data to external services. For example, if you call models through OpenAI function calling in Python, validated inputs prevent bad requests.

Handling Validation Errors

Catch ValidationError to handle failures gracefully. It stores a list of errors with machine-readable details.


from pydantic import ValidationError

try:
    User(id="abc", name="Alice", active=True)
except ValidationError as e:
    print(e.error_count())
    for err in e.errors():
        print(err["loc"], err["msg"])

1
('id',) Input should be a valid integer, unable to parse string as an integer

You can return these errors as JSON in an API response. Clients get clear feedback.

Serializing Models

Pydantic models convert easily to dicts and JSON. This is useful for APIs and logs.


user = User(id=1, name="Alice", active=True)

print(user.model_dump())        # to dict
print(user.model_dump_json())  # to JSON string

{'id': 1, 'name': 'Alice', 'active': True}
{"id":1,"name":"Alice","active":true}

Serialization respects the schema. Only declared fields appear in the output.

Pydantic in Real Pipelines

Data validation is one step in a larger flow. Validate early, then process.

If you parse text before validation, tools like tokenization with NLTK in Python pair well with Pydantic models for clean input.

When services exchange data, schemas matter. If you use compiled .proto files in Python, Pydantic can validate payloads before they reach your protobuf layer.

This keeps errors at the edge of your system, where they are cheap to fix.

Common Mistakes

Using mutable defaults. Avoid lists or dicts as defaults. Use Field(default_factory=list) instead.

Ignoring extra fields. By default, unknown fields are dropped. Set model_config = ConfigDict(extra="forbid") to reject them.

Mixing v1 and v2 syntax. Methods like parse_obj and dict are deprecated. Use model_validate and model_dump.

Conclusion

Pydantic v2 makes data validation simple and fast. You declare types, add constraints, and let the library do the checking.

Start with BaseModel and typed fields. Add Field rules for ranges and lengths. Use custom validators for business logic.

Then handle ValidationError cleanly and serialize with model_dump. Your code will be safer and easier to maintain.