Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Pydantic data validation classes! ๐ In this guide, weโll explore how Pydantic can transform the way you handle data in Python.
Youโll discover how Pydantic makes data validation a breeze, turning potential runtime errors into clear, helpful messages during development. Whether youโre building APIs ๐, processing configuration files ๐, or working with complex data structures ๐๏ธ, understanding Pydantic is essential for writing robust, maintainable Python code.
By the end of this tutorial, youโll feel confident using Pydantic to validate, serialize, and work with data in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Pydantic
๐ค What is Pydantic?
Pydantic is like a super-smart assistant that checks your data before you use it ๐ต๏ธโโ๏ธ. Think of it as a security guard at a concert venue ๐ธ - it checks everyoneโs ticket (data) to make sure theyโre valid before letting them in!
In Python terms, Pydantic provides data validation using Python type annotations. This means you can:
- โจ Automatically validate incoming data
- ๐ Convert data to the right types
- ๐ก๏ธ Catch errors before they cause problems
- ๐ Generate automatic documentation
๐ก Why Use Pydantic?
Hereโs why developers love Pydantic:
- Type Safety ๐: Catch data errors early
- Automatic Conversion ๐: Smart type coercion
- Clear Error Messages ๐ข: Know exactly what went wrong
- JSON Schema Support ๐: Auto-generate API documentation
- Fast Performance โก: Written in Rust for speed
Real-world example: Imagine building an e-commerce API ๐. With Pydantic, you can ensure every order has valid products, quantities, and prices before processing payment!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
# ๐ First, install pydantic: pip install pydantic
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
# ๐จ Creating a simple model
class User(BaseModel):
name: str # ๐ค User's name (required)
age: int # ๐ User's age (required)
email: str # ๐ง Email address
is_active: bool = True # โ
Default to active
joined_at: Optional[datetime] = None # ๐
Optional join date
# ๐ฎ Let's create a user!
user = User(
name="Alice",
age=28,
email="[email protected]"
)
print(f"Welcome {user.name}! ๐")
print(f"Email: {user.email} ๐ง")
๐ก Explanation: Notice how we define types using Pythonโs type hints! Pydantic automatically validates that the data matches these types.
๐ฏ Common Patterns
Here are patterns youโll use daily:
from pydantic import BaseModel, Field, validator
from typing import List, Optional
# ๐๏ธ Pattern 1: Field with constraints
class Product(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0) # ๐ฐ Must be greater than 0
stock: int = Field(default=0, ge=0) # ๐ฆ Can't be negative
# ๐จ Pattern 2: Custom validation
class Order(BaseModel):
items: List[str]
quantity: int
@validator('quantity')
def quantity_must_be_positive(cls, v):
if v <= 0:
raise ValueError('Quantity must be positive! ๐ซ')
return v
# ๐ Pattern 3: Model with relationships
class ShoppingCart(BaseModel):
user_id: int
products: List[Product]
discount_code: Optional[str] = None
class Config:
# ๐ฏ Allow JSON encoding
json_encoders = {
datetime: lambda v: v.isoformat()
}
๐ก Practical Examples
๐ Example 1: E-commerce Order System
Letโs build something real:
from pydantic import BaseModel, Field, validator, EmailStr
from typing import List, Optional
from datetime import datetime
from enum import Enum
# ๐จ Define order status
class OrderStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
# ๐๏ธ Product model
class Product(BaseModel):
id: int
name: str
price: float = Field(..., gt=0, description="Price in USD ๐ต")
quantity: int = Field(..., ge=1)
@property
def total(self) -> float:
return self.price * self.quantity
# ๐ฆ Shipping address
class Address(BaseModel):
street: str
city: str
state: str
zip_code: str = Field(..., regex=r'^\d{5}$') # ๐ฎ US ZIP code
country: str = "USA"
# ๐ Order model
class Order(BaseModel):
order_id: str
customer_email: EmailStr # ๐ง Validates email format!
items: List[Product]
shipping_address: Address
status: OrderStatus = OrderStatus.PENDING
created_at: datetime = Field(default_factory=datetime.now)
notes: Optional[str] = None
@validator('items')
def must_have_items(cls, v):
if not v:
raise ValueError('Order must have at least one item! ๐')
return v
@property
def total_amount(self) -> float:
return sum(item.total for item in self.items)
def ship_order(self):
if self.status == OrderStatus.PENDING:
self.status = OrderStatus.PROCESSING
print(f"๐ฆ Order {self.order_id} is being processed!")
else:
print(f"โ ๏ธ Order already {self.status}")
# ๐ฎ Let's use it!
order_data = {
"order_id": "ORD-001",
"customer_email": "[email protected]",
"items": [
{"id": 1, "name": "Python Book ๐", "price": 29.99, "quantity": 2},
{"id": 2, "name": "Coffee Mug โ", "price": 12.99, "quantity": 1}
],
"shipping_address": {
"street": "123 Python Street",
"city": "Codeville",
"state": "CA",
"zip_code": "12345"
},
"notes": "Please gift wrap! ๐"
}
# ๐ Create order with validation
order = Order(**order_data)
print(f"Order total: ${order.total_amount:.2f} ๐ฐ")
order.ship_order()
๐ฏ Try it yourself: Add a apply_discount
method that validates discount codes!
๐ฎ Example 2: Game Character Builder
Letโs make it fun:
from pydantic import BaseModel, Field, validator
from typing import List, Dict, Optional
from enum import Enum
# ๐ญ Character classes
class CharacterClass(str, Enum):
WARRIOR = "warrior"
MAGE = "mage"
ROGUE = "rogue"
HEALER = "healer"
# ๐จ Character stats
class Stats(BaseModel):
health: int = Field(..., ge=1, le=100)
mana: int = Field(..., ge=0, le=100)
strength: int = Field(..., ge=1, le=20)
intelligence: int = Field(..., ge=1, le=20)
agility: int = Field(..., ge=1, le=20)
# ๐ก๏ธ Equipment
class Equipment(BaseModel):
weapon: Optional[str] = None
armor: Optional[str] = None
accessory: Optional[str] = None
# ๐ฆธ Game character
class GameCharacter(BaseModel):
name: str = Field(..., min_length=3, max_length=20)
character_class: CharacterClass
level: int = Field(default=1, ge=1, le=100)
stats: Stats
equipment: Equipment = Equipment()
inventory: List[str] = []
gold: int = Field(default=100, ge=0)
@validator('name')
def name_must_be_alphanumeric(cls, v):
if not v.replace(' ', '').isalnum():
raise ValueError('Character name must be alphanumeric! ๐ซ')
return v
@validator('stats')
def validate_class_stats(cls, v, values):
if 'character_class' in values:
char_class = values['character_class']
# ๐ก๏ธ Warriors need high strength
if char_class == CharacterClass.WARRIOR and v.strength < 15:
raise ValueError('Warriors need at least 15 strength! ๐ช')
# ๐ง Mages need high intelligence
elif char_class == CharacterClass.MAGE and v.intelligence < 15:
raise ValueError('Mages need at least 15 intelligence! ๐ง ')
return v
def level_up(self):
self.level += 1
self.stats.health += 10
print(f"๐ {self.name} leveled up to {self.level}!")
def add_item(self, item: str):
self.inventory.append(item)
print(f"โจ Added {item} to inventory!")
# ๐ฎ Create a character
warrior_data = {
"name": "Brave Knight",
"character_class": "warrior",
"stats": {
"health": 100,
"mana": 20,
"strength": 18,
"intelligence": 10,
"agility": 12
}
}
hero = GameCharacter(**warrior_data)
hero.add_item("๐ก๏ธ Legendary Sword")
hero.level_up()
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Dynamic Model Creation
When youโre ready to level up, try this advanced pattern:
from pydantic import BaseModel, create_model
from typing import Any, Dict
# ๐ฏ Create models dynamically
def create_api_response_model(data_model: type[BaseModel]) -> type[BaseModel]:
"""Create a standardized API response wrapper"""
return create_model(
f'{data_model.__name__}Response',
success=(bool, True),
data=(data_model, ...),
message=(str, "Success! ๐"),
timestamp=(datetime, Field(default_factory=datetime.now)),
__base__=BaseModel
)
# ๐ช Using dynamic models
UserResponse = create_api_response_model(User)
response = UserResponse(
data=User(name="Bob", age=30, email="[email protected]")
)
print(response.json(indent=2))
# ๐ Dynamic validation
def create_config_model(config_schema: Dict[str, Any]) -> type[BaseModel]:
"""Create a config model from a schema"""
fields = {}
for field_name, field_info in config_schema.items():
if isinstance(field_info, dict):
fields[field_name] = (
field_info.get('type', str),
Field(
default=field_info.get('default', ...),
description=field_info.get('description', '')
)
)
return create_model('DynamicConfig', **fields)
๐๏ธ Advanced Topic 2: Complex Validation and Serialization
For the brave developers:
from pydantic import BaseModel, validator, root_validator
import json
# ๐ Advanced validation patterns
class AdvancedProduct(BaseModel):
name: str
price: float
tags: List[str]
metadata: Dict[str, Any]
# ๐จ Field-level validation
@validator('tags')
def validate_tags(cls, v):
# Remove duplicates and empty strings
return list(set(tag.strip() for tag in v if tag.strip()))
# ๐ Root validation (access all fields)
@root_validator
def validate_product(cls, values):
price = values.get('price', 0)
tags = values.get('tags', [])
# ๐ท๏ธ Premium products need special tag
if price > 100 and 'premium' not in tags:
values['tags'] = tags + ['premium']
return values
# ๐ฏ Custom serialization
class Config:
json_encoders = {
datetime: lambda v: v.strftime('%Y-%m-%d %H:%M:%S'),
float: lambda v: round(v, 2)
}
# ๐ก๏ธ Validate on assignment
validate_assignment = True
# ๐ Use enum values
use_enum_values = True
# ๐ญ Model inheritance
class PremiumProduct(AdvancedProduct):
warranty_years: int = Field(..., ge=1, le=5)
support_level: str = Field(default="gold")
@validator('price')
def price_must_be_premium(cls, v):
if v < 100:
raise ValueError('Premium products must cost at least $100! ๐')
return v
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Forgetting Required Fields
# โ Wrong way - will raise ValidationError!
try:
user = User(name="Alice") # ๐ฐ Missing required fields!
except Exception as e:
print(f"Error: {e}")
# โ
Correct way - provide all required fields!
user = User(
name="Alice",
age=25,
email="[email protected]"
)
print("User created successfully! ๐")
๐คฏ Pitfall 2: Type Confusion
# โ Dangerous - wrong types!
class BadModel(BaseModel):
count: int
# This will fail!
# bad = BadModel(count="not a number") # ๐ฅ ValidationError!
# โ
Safe - Pydantic tries to convert!
class SmartModel(BaseModel):
count: int
price: float
is_active: bool
# These all work thanks to smart conversion! โจ
smart = SmartModel(
count="42", # String โ int
price="19.99", # String โ float
is_active="yes" # String โ bool (truthy)
)
print(f"Count: {smart.count} (type: {type(smart.count)})")
๐ Pitfall 3: Mutable Default Values
# โ Wrong - mutable default!
class BadDefaults(BaseModel):
items: List[str] = [] # ๐ฅ All instances share this list!
# โ
Correct - use Field with default_factory!
from pydantic import Field
class GoodDefaults(BaseModel):
items: List[str] = Field(default_factory=list)
created_at: datetime = Field(default_factory=datetime.now)
config: Dict[str, Any] = Field(default_factory=dict)
๐ ๏ธ Best Practices
- ๐ฏ Use Type Hints: Always specify types for clarity
- ๐ Add Descriptions: Use Field descriptions for documentation
- ๐ก๏ธ Validate Early: Catch errors at the boundaries
- ๐จ Keep Models Focused: One model, one purpose
- โจ Use Config Classes: Customize behavior appropriately
- ๐ Version Your Models: Plan for API evolution
# ๐ Example of best practices
class BestPracticeModel(BaseModel):
# ๐ Clear field descriptions
user_id: int = Field(..., description="Unique user identifier", gt=0)
username: str = Field(..., min_length=3, max_length=50, regex=r'^[a-zA-Z0-9_]+$')
email: EmailStr = Field(..., description="User's email address")
# ๐ฏ Proper configuration
class Config:
# ๐ Generate schema
schema_extra = {
"example": {
"user_id": 123,
"username": "cool_python_dev",
"email": "[email protected]"
}
}
# ๐ก๏ธ Forbid extra fields
extra = "forbid"
# โจ Validate on assignment
validate_assignment = True
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Library Management System
Create a type-safe library management system:
๐ Requirements:
- โ Book model with ISBN validation
- ๐ท๏ธ Categories: fiction, non-fiction, science, history
- ๐ค Member model with borrowing limits
- ๐ Loan tracking with due dates
- ๐จ Each book needs a genre emoji!
๐ Bonus Points:
- Add late fee calculation
- Implement book reservation system
- Create member tier system (silver, gold, platinum)
๐ก Solution
๐ Click to see solution
from pydantic import BaseModel, Field, validator, EmailStr
from typing import List, Optional, Dict
from datetime import datetime, timedelta
from enum import Enum
import re
# ๐ฏ Book categories
class BookCategory(str, Enum):
FICTION = "fiction"
NON_FICTION = "non-fiction"
SCIENCE = "science"
HISTORY = "history"
# ๐ Member tiers
class MemberTier(str, Enum):
SILVER = "silver"
GOLD = "gold"
PLATINUM = "platinum"
# ๐ Book model
class Book(BaseModel):
isbn: str = Field(..., regex=r'^978-\d{10}$')
title: str = Field(..., min_length=1, max_length=200)
author: str
category: BookCategory
genre_emoji: str = Field(..., regex=r'^[\U00010000-\U0010ffff]$')
available: bool = True
@validator('isbn')
def validate_isbn(cls, v):
# Simple ISBN-13 validation
if not v.startswith('978-'):
raise ValueError('ISBN must start with 978- ๐')
return v
# ๐ค Library member
class Member(BaseModel):
member_id: str
name: str
email: EmailStr
tier: MemberTier = MemberTier.SILVER
borrowed_books: List[str] = Field(default_factory=list) # ISBNs
join_date: datetime = Field(default_factory=datetime.now)
@property
def borrowing_limit(self) -> int:
limits = {
MemberTier.SILVER: 3,
MemberTier.GOLD: 5,
MemberTier.PLATINUM: 10
}
return limits[self.tier]
def can_borrow(self) -> bool:
return len(self.borrowed_books) < self.borrowing_limit
# ๐
Loan record
class Loan(BaseModel):
loan_id: str
member_id: str
isbn: str
checkout_date: datetime = Field(default_factory=datetime.now)
due_date: datetime = Field(default_factory=lambda: datetime.now() + timedelta(days=14))
returned_date: Optional[datetime] = None
@property
def is_overdue(self) -> bool:
if self.returned_date:
return False
return datetime.now() > self.due_date
@property
def late_fee(self) -> float:
if not self.is_overdue:
return 0.0
days_late = (datetime.now() - self.due_date).days
return days_late * 0.50 # $0.50 per day
# ๐ Library system
class Library(BaseModel):
name: str
books: Dict[str, Book] = Field(default_factory=dict) # ISBN -> Book
members: Dict[str, Member] = Field(default_factory=dict) # ID -> Member
loans: List[Loan] = Field(default_factory=list)
def add_book(self, book: Book):
self.books[book.isbn] = book
print(f"๐ Added: {book.genre_emoji} {book.title}")
def checkout_book(self, member_id: str, isbn: str) -> Optional[Loan]:
member = self.members.get(member_id)
book = self.books.get(isbn)
if not member:
print("โ Member not found!")
return None
if not book or not book.available:
print("โ Book not available!")
return None
if not member.can_borrow():
print(f"โ Borrowing limit reached ({member.borrowing_limit})!")
return None
# Create loan
loan = Loan(
loan_id=f"L{len(self.loans) + 1:04d}",
member_id=member_id,
isbn=isbn
)
# Update records
book.available = False
member.borrowed_books.append(isbn)
self.loans.append(loan)
print(f"โ
{member.name} borrowed {book.genre_emoji} {book.title}")
print(f"๐
Due date: {loan.due_date.strftime('%Y-%m-%d')}")
return loan
# ๐ฎ Test the system!
library = Library(name="Python Community Library ๐")
# Add books
library.add_book(Book(
isbn="978-1234567890",
title="Learning Python",
author="Guido van Rossum",
category=BookCategory.NON_FICTION,
genre_emoji="๐"
))
# Add member
member = Member(
member_id="M001",
name="Alice Pythonista",
email="[email protected]",
tier=MemberTier.GOLD
)
library.members[member.member_id] = member
# Checkout book
library.checkout_book("M001", "978-1234567890")
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create Pydantic models with confidence ๐ช
- โ Validate data automatically using type hints ๐ก๏ธ
- โ Handle validation errors gracefully ๐ฏ
- โ Build complex data structures with relationships ๐๏ธ
- โ Use advanced features like validators and dynamic models ๐
Remember: Pydantic is your friend in the fight against bad data! It helps you write safer, more maintainable Python code. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Pydantic data validation classes!
Hereโs what to do next:
- ๐ป Practice with the library system exercise above
- ๐๏ธ Add Pydantic to your next API project
- ๐ Explore Pydantic v2โs new features
- ๐ Share your Pydantic models with the community!
Remember: Every Python expert started with their first Pydantic model. Keep coding, keep validating, and most importantly, have fun! ๐
Happy coding! ๐๐โจ