Python Validation with Pydantic Practice Problems & Exercises
Practice: Validation with Pydantic
← Back to lessonDefine a Pydantic BaseModel for a product and observe automatic type coercion.Solution
from pydantic import BaseModel
# TODO: Define a Product BaseModel with these fields:
# - id: int
# - name: str
# - price: float
# - in_stock: bool (default: True)
class Product(BaseModel):
pass
# Pydantic will coerce compatible types (e.g. "3" -> 3)
p = Product(id='1', name='Notebook', price='9.99')
print(f"id type: {type(p.id).__name__}")
print(f"id: {p.id}")
print(f"name: {p.name}")
print(f"price: {p.price}")
print(f"in_stock default: {p.in_stock}")
p2 = Product(id=2, name='Pen', price=1.5, in_stock=False)
print(f"in_stock override: {p2.in_stock}")
Expected Output
id type: int
id: 1
name: Notebook
price: 9.99
in_stock default: True
in_stock override: FalseHints
Hint 1: Define class fields using Python type annotations: field_name: type = default_value.
Hint 2: Pydantic coerces compatible types automatically — '1' becomes 1 for int fields.
Hint 3: Use = True to set a default value; fields without defaults are required.
Define a UserProfile model with optional fields, list fields, and length constraints.Solution
from pydantic import BaseModel, Field
from typing import Optional, List
# TODO: Define a UserProfile model with:
# - username: str (min_length=3, max_length=50 using Field)
# - email: str
# - age: Optional[int] = None
# - interests: List[str] = [] (empty list default)
# - bio: Optional[str] = None
class UserProfile(BaseModel):
pass
# Valid user with all fields
u1 = UserProfile(username='alice42', email='[email protected]', age=28,
interests=['python', 'ml'], bio='Engineer')
print(f"username: {u1.username}")
print(f"age: {u1.age}")
print(f"interests: {u1.interests}")
# Minimal user — optional fields default to None/[]
u2 = UserProfile(username='bob', email='[email protected]')
print(f"age is None: {u2.age is None}")
print(f"interests empty: {u2.interests}")
print(f"bio is None: {u2.bio is None}")
Expected Output
username: alice42
age: 28
interests: ['python', 'ml']
age is None: True
interests empty: []
bio is None: TrueHints
Hint 1: Use Optional[int] = None to make a field optional with a None default.
Hint 2: Use List[str] with a default_factory via Field(default_factory=list) to avoid the mutable default problem.
Hint 3: Field(min_length=3, max_length=50) adds length constraints to string fields.
Catch and inspect a ValidationError when invalid data is passed to a Pydantic model.Solution
from pydantic import BaseModel, Field
from pydantic import ValidationError
class Payment(BaseModel):
amount: float = Field(gt=0)
currency: str = Field(min_length=3, max_length=3)
description: str
# TODO: Try to create an invalid Payment and catch the ValidationError.
# Extract and print the number of validation errors and the first error's field location.
try:
bad = Payment(amount=-10, currency='US', description='')
except ValidationError as e:
print(f"error count: {e.error_count()}")
errors = e.errors()
# Each error has 'loc' (tuple of field names) and 'msg' (description)
print(f"has amount error: {any('amount' in str(err['loc']) for err in errors)}")
print(f"has currency error: {any('currency' in str(err['loc']) for err in errors)}")
print(f"errors is list: {isinstance(errors, list)}")
# Valid payment should not raise
p = Payment(amount=50.0, currency='USD', description='Test payment')
print(f"valid: {p.amount}")
Expected Output
error count: 2
has amount error: True
has currency error: True
errors is list: True
valid: 50.0Hints
Hint 1: Wrap the model instantiation in a try/except ValidationError block.
Hint 2: e.error_count() returns the total number of validation errors.
Hint 3: e.errors() returns a list of dicts, each with 'loc', 'msg', 'type', and 'input' keys.
Export a Pydantic model to dict and JSON, excluding sensitive fields like password_hash.Solution
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
class APIUser(BaseModel):
id: int
username: str
email: str
password_hash: str
created_at: datetime
is_admin: bool = False
user = APIUser(
id=1,
username='alice',
email='[email protected]',
password_hash='$2b$12$abc...',
created_at=datetime(2024, 1, 10, 9, 0, 0),
is_admin=True
)
# TODO:
# 1. Dump to dict — exclude 'password_hash'
# 2. Dump to JSON string — exclude 'password_hash' and 'is_admin'
safe_dict = None # model_dump excluding password_hash
safe_json = None # model_dump_json excluding password_hash and is_admin
print(f"dict has password: {'password_hash' in safe_dict}")
print(f"dict has username: {'username' in safe_dict}")
print(f"dict id: {safe_dict['id']}")
import json
json_data = json.loads(safe_json)
print(f"json has password: {'password_hash' in json_data}")
print(f"json has is_admin: {'is_admin' in json_data}")
print(f"json username: {json_data['username']}")
Expected Output
dict has password: False
dict has username: True
dict id: 1
json has password: False
json has is_admin: False
json username: aliceHints
Hint 1: Use model.model_dump(exclude={'field_name'}) to exclude specific fields from dict output.
Hint 2: Use model.model_dump_json(exclude={'field1', 'field2'}) to exclude fields from JSON output.
Hint 3: model_dump_json() returns a string; parse it with json.loads() to inspect the fields.
Add @field_validator validators to a registration form model that transform and validate field values.Solution
from pydantic import BaseModel, field_validator, Field
from typing import Optional
class RegistrationForm(BaseModel):
username: str
email: str
password: str
age: int = Field(ge=0)
# TODO: Add these validators using @field_validator:
# 1. 'username': strip whitespace, lowercase; raise ValueError if contains spaces after strip
# 2. 'email': raise ValueError if '@' not in the email
# 3. 'password': raise ValueError if len < 8 OR no digit in password
# Valid registration
r = RegistrationForm(username=' Alice ', email='[email protected]',
password='secure123', age=25)
print(f"username lowered/stripped: {r.username}")
print(f"email: {r.email}")
from pydantic import ValidationError
# Bad email
try:
RegistrationForm(username='bob', email='notanemail', password='secure123', age=20)
except ValidationError as e:
print(f"email error: {'email' in str(e)}")
# Short password
try:
RegistrationForm(username='carol', email='[email protected]', password='short', age=30)
except ValidationError as e:
print(f"password error: {'password' in str(e)}")
# Password without digit
try:
RegistrationForm(username='dave', email='[email protected]', password='NoDigitsHere', age=22)
except ValidationError as e:
print(f"no digit error: {'password' in str(e)}")
Expected Output
username lowered/stripped: alice
email: [email protected]
email error: True
password error: True
no digit error: TrueHints
Hint 1: Use @field_validator('username') to decorate a classmethod that takes cls and value as arguments.
Hint 2: Return the transformed value from the validator (e.g. return value.strip().lower()).
Hint 3: Use any(c.isdigit() for c in value) to check for digits in the password.
Build a nested order model with Address and OrderItem sub-models, validating nested data from raw dicts.Solution
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
class Address(BaseModel):
street: str
city: str
country: str = Field(min_length=2, max_length=2) # ISO country code
postal_code: str
class OrderItem(BaseModel):
product_id: int
name: str
quantity: int = Field(ge=1)
unit_price: float = Field(gt=0)
@property
def subtotal(self):
return self.quantity * self.unit_price
class Order(BaseModel):
order_id: str
customer_email: str
shipping_address: Address
items: List[OrderItem]
placed_at: datetime = Field(default_factory=datetime.utcnow)
# TODO: Create an Order with nested Address and multiple OrderItems.
# Use the raw dict approach (Pydantic will auto-validate nested models from dicts).
order_data = {
'order_id': 'ORD-001',
'customer_email': '[email protected]',
'shipping_address': {
'street': '42 Main St',
'city': 'Austin',
'country': 'US',
'postal_code': '78701'
},
'items': [
{'product_id': 1, 'name': 'Widget', 'quantity': 3, 'unit_price': 9.99},
{'product_id': 2, 'name': 'Gadget', 'quantity': 1, 'unit_price': 49.99},
]
}
order = Order(**order_data)
print(f"order_id: {order.order_id}")
print(f"city: {order.shipping_address.city}")
print(f"country: {order.shipping_address.country}")
print(f"item count: {len(order.items)}")
print(f"first item name: {order.items[0].name}")
print(f"first subtotal: {order.items[0].subtotal:.2f}")
print(f"address type: {type(order.shipping_address).__name__}")
Expected Output
order_id: ORD-001
city: Austin
country: US
item count: 2
first item name: Widget
first subtotal: 29.97
address type: AddressHints
Hint 1: Pydantic automatically validates nested dicts against nested model types — no manual conversion needed.
Hint 2: Passing a raw dict for a nested field (e.g. shipping_address) works the same as passing an Address instance.
Hint 3: Use Field(ge=1) for 'greater than or equal to 1' and Field(gt=0) for 'strictly greater than 0'.
Configure Pydantic models with strict=True to disable coercion and frozen=True to make them immutable.Solution
from pydantic import BaseModel, ConfigDict, Field
# TODO: Define two models demonstrating different model_config options:
# 1. StrictConfig: use ConfigDict(strict=True) — disallow type coercion
# (e.g. passing "1" for an int field should raise ValidationError)
class StrictModel(BaseModel):
model_config = ConfigDict(strict=True)
count: int
label: str
# 2. FrozenModel: use ConfigDict(frozen=True) — instances are immutable
# (attempting model.field = value should raise an error)
class FrozenConfig(BaseModel):
model_config = ConfigDict(frozen=True)
x: float
y: float
from pydantic import ValidationError
# Test StrictModel rejects coercion
try:
StrictModel(count='5', label='test')
print("strict coercion: allowed (wrong)")
except ValidationError:
print("strict coercion: rejected (correct)")
# Test StrictModel accepts correct types
m = StrictModel(count=5, label='test')
print(f"strict ok: count={m.count}")
# Test FrozenConfig is immutable
fc = FrozenConfig(x=1.0, y=2.0)
try:
fc.x = 99.0
print("frozen mutation: allowed (wrong)")
except Exception:
print("frozen mutation: rejected (correct)")
print(f"frozen x unchanged: {fc.x}")
Expected Output
strict coercion: rejected (correct)
strict ok: count=5
frozen mutation: rejected (correct)
frozen x unchanged: 1.0Hints
Hint 1: ConfigDict(strict=True) makes Pydantic reject implicit type coercions — '5' will not become 5.
Hint 2: ConfigDict(frozen=True) makes model instances immutable — setting attributes raises ValidationError or TypeError.
Hint 3: Import ConfigDict from pydantic and assign it to model_config as a class attribute.
Add @computed_field properties to cart models that derive subtotals and totals from base fields.Solution
from pydantic import BaseModel, Field, computed_field
from typing import List
class CartItem(BaseModel):
name: str
quantity: int = Field(ge=1)
unit_price: float = Field(gt=0)
@computed_field
@property
def subtotal(self) -> float:
"""Return quantity * unit_price, rounded to 2 decimal places."""
pass
class ShoppingCart(BaseModel):
customer_id: int
items: List[CartItem]
@computed_field
@property
def total(self) -> float:
"""Sum of all item subtotals, rounded to 2 decimal places."""
pass
@computed_field
@property
def item_count(self) -> int:
"""Total quantity of all items."""
pass
cart = ShoppingCart(
customer_id=42,
items=[
{'name': 'Widget', 'quantity': 3, 'unit_price': 9.99},
{'name': 'Gadget', 'quantity': 1, 'unit_price': 24.99},
{'name': 'Doohickey', 'quantity': 2, 'unit_price': 4.50},
]
)
print(f"widget subtotal: {cart.items[0].subtotal}")
print(f"total: {cart.total}")
print(f"item_count: {cart.item_count}")
# Computed fields appear in model_dump()
d = cart.model_dump()
print(f"total in dump: {'total' in d}")
print(f"item_count in dump: {'item_count' in d}")
Expected Output
widget subtotal: 29.97
total: 63.96
item_count: 6
total in dump: True
item_count in dump: TrueHints
Hint 1: Decorate with @computed_field then @property — the order matters (computed_field wraps property).
Hint 2: Computed fields are included in model_dump() output automatically, unlike plain @property.
Hint 3: Round intermediate subtotals before summing to avoid floating-point accumulation: round(q * p, 2).
Use @model_validator(mode='after') to enforce cross-field constraints between start and end dates.Solution
from pydantic import BaseModel, Field, model_validator, ValidationError
from typing import Optional
from datetime import date
class DateRange(BaseModel):
"""A date range where end_date must be after start_date,
and the range cannot exceed max_days (default 365).
"""
start_date: date
end_date: date
max_days: int = Field(default=365, ge=1)
label: Optional[str] = None
# TODO: Add a @model_validator(mode='after') that:
# 1. Raises ValueError if end_date <= start_date
# 2. Raises ValueError if (end_date - start_date).days > max_days
# The validator should run after all fields are set, so use mode='after'.
# Valid range
r = DateRange(start_date=date(2024, 1, 1), end_date=date(2024, 6, 30), label='H1')
print(f"valid: {r.label}, days={(r.end_date - r.start_date).days}")
# end before start
try:
DateRange(start_date=date(2024, 6, 1), end_date=date(2024, 1, 1))
except ValidationError as e:
print(f"end before start: {'end_date' in str(e) or 'start' in str(e).lower()}")
# Range too long
try:
DateRange(start_date=date(2024, 1, 1), end_date=date(2025, 6, 1), max_days=90)
except ValidationError as e:
print(f"too long: {'90' in str(e) or 'max' in str(e).lower()}")
Expected Output
valid: H1, days=181
end before start: True
too long: TrueHints
Hint 1: Decorate with @model_validator(mode='after') — the method receives self (already-validated model instance).
Hint 2: Access fields as self.start_date, self.end_date, self.max_days after validation.
Hint 3: Raise ValueError inside the validator — Pydantic wraps it in a ValidationError automatically.
Build a generic PaginatedResponse[T] Pydantic model that can wrap any item type with pagination metadata.Solution
from pydantic import BaseModel
from typing import TypeVar, Generic, List, Optional
T = TypeVar('T')
# TODO: Define a generic Pydantic model PaginatedResponse[T] that wraps
# any list of items with pagination metadata.
# Fields:
# - items: List[T]
# - total: int (total items across all pages)
# - page: int (current page, 1-indexed)
# - page_size: int (items per page)
# - total_pages: int (computed: ceil(total / page_size))
#
# Also define two concrete item models to test with:
# - UserSummary: id: int, username: str
# - ProductSummary: id: int, name: str, price: float
class UserSummary(BaseModel):
id: int
username: str
class ProductSummary(BaseModel):
id: int
name: str
price: float
class PaginatedResponse(BaseModel, Generic[T]):
pass
import math
users_page = PaginatedResponse[UserSummary](
items=[
UserSummary(id=1, username='alice'),
UserSummary(id=2, username='bob'),
],
total=25,
page=1,
page_size=2,
total_pages=math.ceil(25 / 2),
)
products_page = PaginatedResponse[ProductSummary](
items=[ProductSummary(id=10, name='Widget', price=9.99)],
total=5,
page=2,
page_size=3,
total_pages=math.ceil(5 / 3),
)
print(f"users count: {len(users_page.items)}")
print(f"first user: {users_page.items[0].username}")
print(f"users total_pages: {users_page.total_pages}")
print(f"product name: {products_page.items[0].name}")
print(f"products page: {products_page.page}")
print(f"products total_pages: {products_page.total_pages}")
Expected Output
users count: 2
first user: alice
users total_pages: 13
product name: Widget
products page: 2
products total_pages: 2Hints
Hint 1: Inherit from both BaseModel and Generic[T]: class PaginatedResponse(BaseModel, Generic[T]).
Hint 2: Use List[T] as the type annotation for the items field.
Hint 3: Generic Pydantic models work the same as regular ones — just parameterized with a concrete type at instantiation.
Build a production-quality API request model with multiple field validators, a cross-field model validator, and a frozen immutable response model.Solution
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict, ValidationError
from typing import Optional, List
from datetime import datetime
from enum import Enum
class Priority(str, Enum):
LOW = 'low'
MEDIUM = 'medium'
HIGH = 'high'
CRITICAL = 'critical'
# --- Request Model ---
class CreateTaskRequest(BaseModel):
"""Strict API request model for creating a task.
Validation rules:
- title: 1-200 chars, strip whitespace, non-empty after stripping
- description: optional, max 2000 chars
- priority: must be a valid Priority enum value
- due_date: optional; if provided, must be in the future
- tags: list of lowercase strings; duplicates removed; max 10 tags
- assigned_to: optional list of positive user IDs; max 5 assignees
"""
title: str = Field(min_length=1, max_length=200)
description: Optional[str] = Field(default=None, max_length=2000)
priority: Priority = Priority.MEDIUM
due_date: Optional[datetime] = None
tags: List[str] = Field(default_factory=list)
assigned_to: List[int] = Field(default_factory=list)
# TODO: Add these validators:
# @field_validator('title'): strip whitespace; raise ValueError if empty after strip
# @field_validator('tags'): lowercase all tags, deduplicate (preserve first occurrence order),
# raise ValueError if more than 10 tags
# @field_validator('assigned_to'): raise ValueError if any ID <= 0 or if more than 5 assignees
# @model_validator(mode='after'): raise ValueError if due_date is set and is in the past
# --- Response Model ---
class TaskResponse(BaseModel):
model_config = ConfigDict(frozen=True)
id: int
title: str
priority: Priority
due_date: Optional[datetime]
tags: List[str]
created_at: datetime
status: str = 'open'
# --- Test valid request ---
req = CreateTaskRequest(
title=' Fix critical bug ',
priority='high',
tags=['Python', 'python', 'API', 'api', 'backend'],
assigned_to=[1, 2, 3],
due_date=datetime(2099, 12, 31),
)
print(f"title stripped: '{req.title}'")
print(f"priority: {req.priority.value}")
print(f"tags deduped: {req.tags}")
print(f"assignees: {req.assigned_to}")
# --- Test invalid: past due_date ---
try:
CreateTaskRequest(title='Old task', due_date=datetime(2000, 1, 1))
except ValidationError as e:
print(f"past due_date: {'due_date' in str(e) or 'past' in str(e).lower()}")
# --- Test invalid: bad user ID ---
try:
CreateTaskRequest(title='Task', assigned_to=[1, -5, 3])
except ValidationError as e:
print(f"bad user id: {'assigned_to' in str(e) or 'positive' in str(e).lower()}")
# --- Test response is frozen ---
resp = TaskResponse(
id=1, title='Fix critical bug', priority=Priority.HIGH,
due_date=None, tags=['api', 'backend'],
created_at=datetime(2024, 6, 1)
)
try:
resp.status = 'closed'
print("frozen: mutable (wrong)")
except Exception:
print("frozen: immutable (correct)")
print(f"resp title: {resp.title}")
Expected Output
title stripped: 'Fix critical bug'
priority: high
tags deduped: ['python', 'api', 'backend']
assignees: [1, 2, 3]
past due_date: True
bad user id: True
frozen: immutable (correct)
resp title: Fix critical bugHints
Hint 1: For tag deduplication, use dict.fromkeys(tags) to preserve insertion order while removing duplicates.
Hint 2: For the due_date validator, compare against datetime.utcnow() inside a @model_validator(mode='after').
Hint 3: Use @field_validator('assigned_to') and check all(uid > 0 for uid in value) before checking length.
