Skip to main content

Getting Started: Your First Pydantic Models

A Pydantic model is a Python class that inherits from BaseModel and declares typed fields. When you instantiate the model with data, Pydantic validates each field against its type and constraints, raising ValidationError if anything is invalid. This is how you gain certainty: one line of code replaces dozens of defensive checks.

This tutorial walks you through creating your first model, understanding how validation works, and using the error output to debug mismatches. By the end, you'll be comfortable using Pydantic in your own code.

What Is a Pydantic Model?

A Pydantic model is simply a class with typed attributes. Inherit from BaseModel, declare fields with type annotations, and Pydantic handles the rest:

from pydantic import BaseModel

class Product(BaseModel):
name: str
price: float
in_stock: bool

# Valid instantiation
product = Product(name="Laptop", price=999.99, in_stock=True)
print(product.name) # "Laptop"
print(product.price) # 999.99

# Access as attribute or dict
print(product.model_dump()) # {'name': 'Laptop', 'price': 999.99, 'in_stock': True}

When you create a Product instance, Pydantic:

  1. Checks that name is a string (or coerces a compatible type).
  2. Checks that price is a float.
  3. Checks that in_stock is a boolean.

If any field fails validation, Pydantic raises a ValidationError with a detailed report.

Understanding Validation Errors

Validation errors are helpful. Let's see what happens when you pass invalid data:

from pydantic import ValidationError, BaseModel

class Product(BaseModel):
name: str
price: float
in_stock: bool

try:
# price is missing, in_stock is a string instead of bool
product = Product(name="Monitor", price="invalid")
except ValidationError as e:
print(e.json())
# Output:
# [
# {
# "type": "missing",
# "loc": ["price"],
# "msg": "Field required",
# "input": {
# "name": "Monitor"
# }
# },
# {
# "type": "string_type",
# "loc": ["price"],
# "msg": "Input should be a valid number, unable to interpret as an integer",
# "input": "invalid"
# }
# ]

Each error includes:

  • type: The validation error category (e.g., missing, string_type, value_error).
  • loc: The path to the field (useful for nested models).
  • msg: A human-readable message.
  • input: The data that failed.

This structured output is perfect for API error responses—you can send the entire error list to the client, and they immediately know what to fix.

Optional Fields and Defaults

By default, all fields are required. To make a field optional, use Optional[Type] or provide a default value:

from typing import Optional
from pydantic import BaseModel

class Article(BaseModel):
title: str
body: str
author: Optional[str] = None # Optional with default None
views: int = 0 # Has a default, so it's not required

# Valid: only title and body required
article = Article(title="Python Tips", body="Here are 5 tips...")
print(article.author) # None
print(article.views) # 0

# Also valid: provide optional fields
article2 = Article(title="Tips", body="...", author="Alex", views=1500)

Required vs. optional is now explicit in the type signature—no more guessing from the code.

Type Coercion and Strictness

Pydantic coerces compatible types. A string "42" becomes int 42, and a dict becomes a nested model instance. This is convenient, but occasionally too permissive:

from pydantic import BaseModel

class Order(BaseModel):
quantity: int
total: float

# Coercion happens automatically
order = Order(quantity="5", total="99.99")
print(order.quantity, order.total) # 5, 99.99 (types correct)

# But this fails: string "hello" cannot become int
try:
bad = Order(quantity="hello", total="99.99")
except Exception as e:
print(type(e).__name__) # ValidationError

Coercion is a design choice. In Pydantic v2, you control strictness per field using Field(strict=True) or by configuring the model with ConfigDict(strict=True).

Serialization and Export

Once validated, serialize your model to JSON, dict, or other formats:

from pydantic import BaseModel

class Author(BaseModel):
name: str
email: str

author = Author(name="Dr. Alex Turner", email="[email protected]")

# Export as dict
print(author.model_dump())
# {'name': 'Dr. Alex Turner', 'email': '[email protected]'}

# Export as JSON string
print(author.model_dump_json())
# '{"name":"Dr. Alex Turner","email":"[email protected]"}'

# Export with custom settings
print(author.model_dump(exclude={"email"}))
# {'name': 'Dr. Alex Turner'}

Serialization is essential for APIs—it ensures your response JSON is clean, consistent, and matches your schema.

Model Configuration and Behavior

You can customize a model's behavior via ConfigDict:

from pydantic import BaseModel, ConfigDict

class Settings(BaseModel):
model_config = ConfigDict(
strict=False, # Allow coercion
str_strip_whitespace=True, # Trim strings
validate_default=True, # Validate default values
)

app_name: str
debug: bool = False

settings = Settings(app_name=" MyApp ", debug="true")
print(settings.app_name) # "MyApp" (whitespace stripped)
print(settings.debug) # True (string coerced to bool)

Common configurations include:

  • strict: Disable type coercion; require exact types.
  • str_strip_whitespace: Remove leading/trailing whitespace from strings.
  • validate_default: Validate default values at model definition time (catches bugs early).
  • json_schema_extra: Add custom JSON schema properties.

From Dict to Model and Back

One of Pydantic's superpowers is converting between Python dicts and models seamlessly:

from pydantic import BaseModel

class Task(BaseModel):
id: int
title: str
completed: bool = False

# Create from dict (e.g., from JSON API response)
data = {"id": 1, "title": "Build API", "completed": False}
task = Task(**data) # Unpack dict as kwargs

# Convert back to dict for JSON serialization
api_response = task.model_dump()

# Validate and transform in one step
api_payload = {"id": 2, "title": "Write tests"}
task2 = Task(**api_payload) # Raises ValidationError if invalid

print(task2.completed) # False (default applied)

This dict-to-model-to-dict cycle is the foundation of Pydantic's integration with web frameworks—FastAPI receives a dict from JSON, validates it as a Pydantic model, and returns a model that FastAPI serializes back to JSON.

Key Takeaways

  • Inherit from BaseModel and declare fields with type annotations to create a Pydantic model.
  • Pydantic validates on instantiation and raises ValidationError if data is invalid.
  • Use Optional[Type] or defaults to mark fields as non-required.
  • Pydantic coerces compatible types automatically (string "42" becomes int 42).
  • Export models to dict or JSON with model_dump() or model_dump_json().
  • Configure model behavior via ConfigDict to control strictness, whitespace handling, and validation.

Frequently Asked Questions

What happens if I don't provide a required field?

Pydantic raises ValidationError with a missing error for that field. The error message is clear: "Field required." In API contexts, return this error to the client with a 422 status code.

Can I inherit from another Pydantic model?

Yes. Child classes inherit parent fields and can add their own. This is useful for API request/response DTOs that share a base structure.

How does Pydantic handle None values?

If a field is typed as str (not Optional[str]), passing None raises ValidationError. Use Optional[Type] to explicitly allow None.

Do I need to define __init__ or other methods?

No. BaseModel provides __init__, __repr__, __eq__, and other standard methods. You rarely override them unless you need custom behavior.

How fast is model instantiation?

Pydantic v2 is extremely fast—on typical hardware, instantiating a 20-field model takes 10-50 microseconds. For comparison, manual validation of the same data takes 100-500 microseconds. Pydantic is faster because the Rust core compiles validators once.

Further Reading