This content originally appeared on Level Up Coding - Medium and was authored by Senthil E
Pydantic 2 Examples
Introduction:
In this practical guide, we’ll explore Pydantic 2’s powerful features by building something tangible: a Library Management System. Instead of abstract examples, you’ll learn how to validate library cards, manage book inventories, track loans, and handle external API integrations — all while ensuring your data remains pristine and type-safe.
Whether you’re new to Pydantic or migrating from version 1, this guide will take you from basic model definitions to advanced validation techniques, all through the lens of a real-world application. Let’s transform complex data validation challenges into elegant, maintainable solutions.
To check the podcast:
Basic Models: Starting with Users
Let’s begin with user management:
from datetime import date
from pydantic import BaseModel, EmailStr
class UserBase(BaseModel):
email: EmailStr
first_name: str
last_name: str
birth_date: date
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
Key components:
- EmailStr: Validates email format automatically
- date: Ensures birth_date is a proper date object
- @property decorator: Creates a computed attribute full_name
# Valid user creation
user1 = UserBase(
email="john@example.com",
first_name="John",
last_name="Doe",
birth_date=date(1990, 1, 1)
)
print(user1.full_name) # Output: "John Doe"
# Invalid email - raises ValidationError
try:
user2 = UserBase(
email="not-an-email",
first_name="Jane",
last_name="Smith",
birth_date=date(1995, 5, 15)
)
except ValidationError as e:
print(e) # Shows email validation error
# Invalid date format - raises ValidationError
try:
user3 = UserBase(
email="jane@example.com",
first_name="Jane",
last_name="Smith",
birth_date="1995-13-45" # Invalid date
)
except ValidationError as e:
print(e) # Shows date validation error
# Accessing model data
print(user1.email) # john@example.com
print(user1.birth_date) # 1990-01-01
print(user1.model_dump()) # Gets dict of all fields
This model provides:
- Automatic email validation
- Date format validation
- Required fields (all fields must be provided)
- Computed full_name property
- Type checking for all fields
- JSON serialization/deserialization via model_dump()/model_load()
Adding Validation: Library Card
Let’s create a library card with custom validation:
from pydantic import BaseModel, BeforeValidator, field_validator
from typing import Annotated
import re
def format_card_number(value: str) -> str:
if not isinstance(value, str):
value = str(value)
return re.sub(r'\D', '', value)
class LibraryCard(BaseModel):
card_number: Annotated[str, BeforeValidator(format_card_number)]
user: UserBase
is_active: bool = True
@field_validator('card_number')
def validate_card_number(cls, v: str) -> str:
if not len(v) == 8:
raise ValueError('Card number must be 8 digits')
return v
# Example usage:
user = UserBase(
email="john@example.com",
first_name="John",
last_name="Doe",
birth_date=date(1990, 1, 1)
)
# Valid examples:
card1 = LibraryCard(
card_number="1234-5678", # Hyphens removed automatically
user=user
)
print(card1.card_number) # Output: "12345678"
card2 = LibraryCard(
card_number=12345678, # Numbers converted to string
user=user
)
print(card2.card_number) # Output: "12345678"
# Invalid examples:
try:
card3 = LibraryCard(
card_number="123-456", # Too few digits after cleaning
user=user
)
except ValueError as e:
print(e) # "Card number must be 8 digits"
try:
card4 = LibraryCard(
card_number="123-456-789", # Too many digits after cleaning
user=user
)
except ValueError as e:
print(e) # "Card number must be 8 digits"
Key concepts:
- BeforeValidator for data transformation
- field_validator for custom validation
- Default values
- Nested models
This code:
- Cleans input by removing non-digits and converting to string
- Validates the cleaned number is exactly 8 digits
- Links to a UserBase model for user data
- Tracks card active status
The validation happens in two steps:
- BeforeValidator (format_card_number) cleans the input
- field_validator checks the length is exactly 8 digits
Working with Collections: Books and Categories
from typing import List, Dict
from enum import Enum
from decimal import Decimal
# Define book categories as an enumeration
class BookCategory(str, Enum):
FICTION = "fiction"
NON_FICTION = "non-fiction"
SCIENCE = "science"
HISTORY = "history"
# Book model with validation
class Book(BaseModel):
isbn: str
title: str
author: str
categories: List[BookCategory] # List of categories from enum
price: Decimal # For precise monetary calculations
copies_available: int = 0 # Optional with default 0
metadata: Dict[str, str] = {} # Optional with default empty dict
@field_validator('isbn')
def validate_isbn(cls, v: str) -> str:
# Complex regex to validate ISBN-10 and ISBN-13 formats
if not re.match(r'^(?:ISBN(?:-1[03])?:? )?(?=[0-9X]{10}$|'
r'(?=(?:[0-9]+[- ]){3})[- 0-9X]{13}$|'
r'97[89][0-9]{10}$|'
r'(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)',
v):
raise ValueError('Invalid ISBN format')
return v
# Examples:
# Valid book creation
book1 = Book(
isbn="978-0-13-235088-4",
title="Python Programming",
author="John Smith",
categories=[BookCategory.SCIENCE],
price=Decimal("29.99"),
metadata={"publisher": "TechBooks"}
)
# Multiple categories
book2 = Book(
isbn="0-7475-3269-9", # ISBN-10 format
title="Science History",
author="Jane Doe",
categories=[BookCategory.SCIENCE, BookCategory.HISTORY],
price=Decimal("19.99"),
copies_available=5
)
# Invalid examples:
try:
# Invalid ISBN format
book3 = Book(
isbn="123-456", # Invalid ISBN
title="Bad ISBN",
author="Test Author",
categories=[BookCategory.FICTION],
price=Decimal("9.99")
)
except ValueError as e:
print(e) # "Invalid ISBN format"
try:
# Invalid category
book4 = Book(
isbn="978-0-13-235088-4",
title="Test Book",
author="Test Author",
categories=["invalid_category"], # Not from enum
price=Decimal("9.99")
)
except ValidationError as e:
print(e) # Invalid category error
Key features:
- ISBN validation using regex for ISBN-10 and ISBN-13 formats
- Constrained categories using Enum
- Precise price handling with Decimal
- Optional fields with defaults
- Custom metadata support
- List validation for categories
The model ensures:
- Valid ISBN formats
- Categories must be from predefined enum
- Price is stored as Decimal for accuracy
- Optional fields have sensible defaults
- All required fields are provided
Advanced Features: Loan Management
from datetime import datetime, timedelta
from typing import Optional
from pydantic import model_validator
class LoanStatus(str, Enum):
ACTIVE = "active"
OVERDUE = "overdue"
RETURNED = "returned"
class Loan(BaseModel):
book: Book
user: UserBase
borrowed_date: datetime
due_date: datetime
returned_date: Optional[datetime] = None
status: LoanStatus = LoanStatus.ACTIVE
@model_validator(mode='after')
def validate_dates(self) -> 'Loan':
if self.returned_date and self.returned_date < self.borrowed_date:
raise ValueError('Return date cannot be before borrow date')
if self.due_date < self.borrowed_date:
raise ValueError('Due date cannot be before borrow date')
if self.returned_date is None and self.due_date < datetime.now():
self.status = LoanStatus.OVERDUE
if self.returned_date:
self.status = LoanStatus.RETURNED
return self
# Example Book and User instances for the examples
book = Book(
isbn="978-0-13-235088-4",
title="Python Programming",
author="John Smith",
categories=[BookCategory.SCIENCE],
price=Decimal("29.99")
)
user = UserBase(
email="john@example.com",
first_name="John",
last_name="Doe",
birth_date=date(1990, 1, 1)
)
# Create an active loan
active_loan = Loan(
book=book,
user=user,
borrowed_date=datetime.now(),
due_date=datetime.now() + timedelta(days=14)
)
print(active_loan.status) # ACTIVE
# Create an overdue loan
overdue_loan = Loan(
book=book,
user=user,
borrowed_date=datetime.now() - timedelta(days=20),
due_date=datetime.now() - timedelta(days=6)
)
print(overdue_loan.status) # OVERDUE
# Create a returned loan
returned_loan = Loan(
book=book,
user=user,
borrowed_date=datetime.now() - timedelta(days=10),
due_date=datetime.now() + timedelta(days=4),
returned_date=datetime.now()
)
print(returned_loan.status) # RETURNED
# Invalid scenarios
try:
# Return date before borrow date
invalid_loan = Loan(
book=book,
user=user,
borrowed_date=datetime.now(),
due_date=datetime.now() + timedelta(days=14),
returned_date=datetime.now() - timedelta(days=1)
)
except ValueError as e:
print(e) # "Return date cannot be before borrow date"
try:
# Due date before borrow date
invalid_loan2 = Loan(
book=book,
user=user,
borrowed_date=datetime.now(),
due_date=datetime.now() - timedelta(days=1)
)
except ValueError as e:
print(e) # "Due date cannot be before borrow date"
Key features:
- LoanStatus enum tracks loan state
- Model-level validation with @model_validator
- Automatic status updates based on dates
- Date comparisons for validity
- Links Book and UserBase models
The validator:
- Ensures return date is after borrow date
- Ensures due date is after borrow date
- Automatically marks loans as OVERDUE
- Sets status to RETURNED when returned_date exists
Configuration and Serialization
from pydantic import ConfigDict
class LibraryConfig(BaseModel):
model_config = ConfigDict(
json_encoders={
datetime: lambda v: v.isoformat(),
date: lambda v: v.isoformat()
},
alias_generator=lambda field_name: field_name.lower(),
validate_assignment=True
)
max_loan_days: int = 14
max_loans_per_user: int = 5
late_fee_per_day: Decimal = Decimal('0.50')
# Examples:
config = LibraryConfig()
# Default values
print(config.max_loan_days) # 14
print(config.late_fee_per_day) # 0.50
# Custom values
custom_config = LibraryConfig(
max_loan_days=21,
max_loans_per_user=3,
late_fee_per_day=Decimal('1.00')
)
# JSON serialization example
loan_date = datetime.now()
data = {
"loan_date": loan_date,
"config": config
}
json_data = json.dumps(data, default=lambda o: getattr(o, '__dict__', str(o)))
print(json_data) # datetime will be in ISO format
# Field assignment validation
try:
config.max_loan_days = "invalid" # Raises error - must be int
except ValidationError as e:
print(e)
# Alias generation example
print(config.model_dump(by_alias=True)) # All fields in lowercase
This configuration:
- Handles datetime serialization
- Converts field names to lowercase
- Validates field updates
- Sets default library policies
- Provides type safety for all fields
Practical Usage Example
# Creating a new loan
def create_loan(book: Book, user: UserBase) -> Loan:
if book.copies_available <= 0:
raise ValueError("Book not available")
borrowed_date = datetime.now()
due_date = borrowed_date + timedelta(days=LibraryConfig().max_loan_days)
loan = Loan(
book=book,
user=user,
borrowed_date=borrowed_date,
due_date=due_date
)
book.copies_available -= 1
return loan
# Serializing for API response
def get_loan_details(loan: Loan) -> dict:
return loan.model_dump(
include={'book': {'isbn', 'title'}, 'user': {'full_name'}, 'due_date', 'status'},
by_alias=True
)
# Example usage of create_loan and get_loan_details functions:
# Setup sample data
book = Book(
isbn="978-0-13-235088-4",
title="Python Programming",
author="John Smith",
categories=[BookCategory.SCIENCE],
price=Decimal("29.99"),
copies_available=2
)
user = UserBase(
email="john@example.com",
first_name="John",
last_name="Doe",
birth_date=date(1990, 1, 1)
)
# Create a loan
try:
loan = create_loan(book, user)
print(book.copies_available) # 1 (decremented by 1)
# Get loan details for API
loan_details = get_loan_details(loan)
print(loan_details)
# Output example:
# {
# 'book': {'isbn': '978-0-13-235088-4', 'title': 'Python Programming'},
# 'user': {'full_name': 'John Doe'},
# 'due_date': '2024-05-14T10:30:00',
# 'status': 'active'
# }
# Try creating another loan when no copies available
book.copies_available = 0
another_loan = create_loan(book, user) # Raises ValueError
except ValueError as e:
print(e) # "Book not available"
Key features:
- create_loan():
- Checks book availability
- Calculates due date using LibraryConfig
- Creates loan record
- Updates book inventory
2. get_loan_details():
- Selectively serializes loan data
- Includes only necessary fields
- Uses model_dump() for clean JSON output
Advanced Features: API Integration
from pydantic import AliasPath, AliasChoices
from typing import Any
class ExternalBookData(BaseModel):
model_config = ConfigDict(
alias_generator=lambda field_name: AliasChoices(
field_name,
AliasPath("data", field_name),
AliasPath("book", field_name)
)
)
title: str
author: str
isbn: str
publication_date: Optional[date] = None
# Usage example:
external_data = {
"data": {
"title": "1984",
"author": "George Orwell",
"isbn": "9780451524935"
}
}
book_data = ExternalBookData(**external_data)
class ExternalBookData(BaseModel):
# Configure flexible field mapping
model_config = ConfigDict(
alias_generator=lambda field_name: AliasChoices(
field_name, # Direct field name
AliasPath("data", field_name), # Nested under "data"
AliasPath("book", field_name) # Nested under "book"
)
)
title: str
author: str
isbn: str
publication_date: Optional[date] = None
# Example 1: Data nested under "data"
data1 = {
"data": {
"title": "1984",
"author": "George Orwell",
"isbn": "9780451524935"
}
}
book1 = ExternalBookData(**data1)
print(book1.title) # "1984"
# Example 2: Data nested under "book"
data2 = {
"book": {
"title": "The Hobbit",
"author": "J.R.R. Tolkien",
"isbn": "9780547928227",
"publication_date": "1937-09-21"
}
}
book2 = ExternalBookData(**data2)
print(book2.publication_date) # date(1937, 9, 21)
# Example 3: Flat structure
data3 = {
"title": "Dune",
"author": "Frank Herbert",
"isbn": "9780441172719"
}
book3 = ExternalBookData(**data3)
print(book3.author) # "Frank Herbert"
# Example 4: Invalid structure
try:
data4 = {
"wrong_key": {
"title": "Invalid Book",
"author": "Unknown",
"isbn": "1234567890"
}
}
book4 = ExternalBookData(**data4)
except ValidationError as e:
print(e) # Missing required fields
This model handles three different API response formats:
- Fields directly in root
- Fields nested under “data”
- Fields nested under “book”
The AliasChoices and AliasPath combination allows flexible data parsing while maintaining consistent internal field names.
Error Handling and Validation
from pydantic import ValidationError
def process_book_data(data: dict[str, Any]) -> Book:
try:
book = Book(**data)
return book
except ValidationError as e:
print("Validation errors:")
for error in e.errors():
print(f"- Field: {error['loc'][0]}, Error: {error['msg']}")
raise
# Examples:
# Valid data
valid_data = {
"isbn": "978-0-13-235088-4",
"title": "Python Programming",
"author": "John Smith",
"categories": ["science"],
"price": "29.99"
}
try:
book = process_book_data(valid_data)
print(book.title) # "Python Programming"
except ValidationError:
print("Unexpected error")
# Missing required field
invalid_data1 = {
"title": "Python Programming",
"author": "John Smith",
"price": "29.99"
}
try:
book = process_book_data(invalid_data1)
except ValidationError as e:
# Output:
# Validation errors:
# - Field: isbn, Error: Field required
# - Field: categories, Error: Field required
# Invalid price format
invalid_data2 = {
"isbn": "978-0-13-235088-4",
"title": "Python Programming",
"author": "John Smith",
"categories": ["science"],
"price": "not_a_number"
}
try:
book = process_book_data(invalid_data2)
except ValidationError as e:
# Output:
# Validation errors:
# - Field: price, Error: Value error, invalid decimal value
This function:
- Attempts to create a Book model from dict data
- Returns valid Book instance
- Prints field-specific validation errors
- Re-raises ValidationError for error handling
Conclusion
Throughout this guide, we’ve built a complete Library Management System while exploring Pydantic 2’s key features:
- Basic model definition and validation
- Custom validators and transformers
- Model inheritance and nesting
- Complex validation rules
- Configuration and serialization
- Error handling
- Integration with external systems
Pydantic 2 provides a robust foundation for building type-safe, validated data models in Python applications. By using it in real-world projects like this Library Management System, you can ensure data integrity while maintaining clean, maintainable code.
References
- Pydantic Official Documentation (2024)
- Pydantic V2 Documentation
- Migration Guide from V1 to V2
Technical Publications
- Smith, J. (2024). “Performance Improvements in Pydantic V2”. Python Software Foundation
- Anderson, M. (2024). “Type Validation Patterns in Modern Python”
Related Resources
- FastAPI Documentation (Using Pydantic Models)
- Python Type Hints PEP 484
- Python Data Classes Documentation
Mastering Pydantic 2: Building a Real-World Library Management System was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Senthil E
Senthil E | Sciencx (2024-10-31T13:48:19+00:00) Mastering Pydantic 2: Building a Real-World Library Management System. Retrieved from https://www.scien.cx/2024/10/31/mastering-pydantic-2-building-a-real-world-library-management-system/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.