Skip to main content

Python Validation with Pydantic Practice Problems & Exercises

Practice: Validation with Pydantic

11 problems4 Easy4 Medium3 Hard55–70 min
← Back to lesson

#1Basic BaseModel DefinitionEasy
pydanticBaseModeltype coercionvalidation

Define a Pydantic BaseModel for a product and observe automatic type coercion.

Solution
from pydantic import BaseModel

class Product(BaseModel):
id: int
name: str
price: float
in_stock: bool = True

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}")
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: False
Hints

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.


#2Field Types and Optional FieldsEasy
pydanticOptionalListFielddefault

Define a UserProfile model with optional fields, list fields, and length constraints.

Solution
from pydantic import BaseModel, Field
from typing import Optional, List

class UserProfile(BaseModel):
username: str = Field(min_length=3, max_length=50)
email: str
age: Optional[int] = None
interests: List[str] = Field(default_factory=list)
bio: Optional[str] = None

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}")

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}")
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: True
Hints

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.


#3ValidationError HandlingEasy
pydanticValidationErrorerror messageserror details

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

try:
bad = Payment(amount=-10, currency='US', description='')
except ValidationError as e:
print(f"error count: {e.error_count()}")
errors = e.errors()
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)}")

p = Payment(amount=50.0, currency='USD', description='Test payment')
print(f"valid: {p.amount}")
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.0
Hints

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.


#4model_dump() and model_dump_json()Easy
pydanticmodel_dumpmodel_dump_jsonserializationexclude

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
import json

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',
password_hash='$2b$12$abc...',
created_at=datetime(2024, 1, 10, 9, 0, 0),
is_admin=True
)

safe_dict = user.model_dump(exclude={'password_hash'})
safe_json = user.model_dump_json(exclude={'password_hash', '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']}")

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']}")
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: alice
Hints

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.


#5Field Validators with @field_validatorMedium
pydanticfield_validatorcustom validationbefore/after

Add @field_validator validators to a registration form model that transform and validate field values.

Solution
from pydantic import BaseModel, field_validator, Field, ValidationError
from typing import Optional

class RegistrationForm(BaseModel):
username: str
email: str
password: str
age: int = Field(ge=0)

@field_validator('username')
@classmethod
def validate_username(cls, value):
value = value.strip().lower()
if ' ' in value:
raise ValueError('Username must not contain spaces')
return value

@field_validator('email')
@classmethod
def validate_email(cls, value):
if '@' not in value:
raise ValueError('Invalid email address')
return value

@field_validator('password')
@classmethod
def validate_password(cls, value):
if len(value) < 8:
raise ValueError('Password must be at least 8 characters')
if not any(c.isdigit() for c in value):
raise ValueError('Password must contain at least one digit')
return value

r = RegistrationForm(username=' Alice ', email='[email protected]',
password='secure123', age=25)
print(f"username lowered/stripped: {r.username}")
print(f"email: {r.email}")

try:
RegistrationForm(username='bob', email='notanemail', password='secure123', age=20)
except ValidationError as e:
print(f"email error: {'email' in str(e)}")

try:
RegistrationForm(username='carol', email='[email protected]', password='short', age=30)
except ValidationError as e:
print(f"password error: {'password' in str(e)}")

try:
RegistrationForm(username='dave', email='[email protected]', password='NoDigitsHere', age=22)
except ValidationError as e:
print(f"no digit error: {'password' in str(e)}")
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: True
Hints

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.


#6Nested ModelsMedium
pydanticnested modelscompositionvalidation

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)
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)

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__}")
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: Address
Hints

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'.


#7model_config SettingsMedium
pydanticmodel_configConfigDictstrictfrozenalias

Configure Pydantic models with strict=True to disable coercion and frozen=True to make them immutable.

Solution
from pydantic import BaseModel, ConfigDict, ValidationError

class StrictModel(BaseModel):
model_config = ConfigDict(strict=True)
count: int
label: str

class FrozenConfig(BaseModel):
model_config = ConfigDict(frozen=True)
x: float
y: float

try:
StrictModel(count='5', label='test')
print("strict coercion: allowed (wrong)")
except ValidationError:
print("strict coercion: rejected (correct)")

m = StrictModel(count=5, label='test')
print(f"strict ok: count={m.count}")

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}")
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.0
Hints

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.


#8Computed FieldsMedium
pydanticcomputed_fieldpropertyderived values

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 round(self.quantity * self.unit_price, 2)

class ShoppingCart(BaseModel):
customer_id: int
items: List[CartItem]

@computed_field
@property
def total(self) -> float:
return round(sum(item.subtotal for item in self.items), 2)

@computed_field
@property
def item_count(self) -> int:
return sum(item.quantity for item in self.items)

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}")

d = cart.model_dump()
print(f"total in dump: {'total' in d}")
print(f"item_count in dump: {'item_count' in d}")
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: True
Hints

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).


#9Custom Root Validator with @model_validatorHard
pydanticmodel_validatorcross-field validationmode=after

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):
start_date: date
end_date: date
max_days: int = Field(default=365, ge=1)
label: Optional[str] = None

@model_validator(mode='after')
def validate_range(self):
if self.end_date <= self.start_date:
raise ValueError(
f"end_date ({self.end_date}) must be after start_date ({self.start_date})"
)
delta = (self.end_date - self.start_date).days
if delta > self.max_days:
raise ValueError(
f"Range of {delta} days exceeds max_days={self.max_days}"
)
return self

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}")

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()}")

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()}")
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: True
Hints

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.


#10Generic Model with TypeVarHard
pydanticgeneric modelTypeVarGenericpagination

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
import math

T = TypeVar('T')

class UserSummary(BaseModel):
id: int
username: str

class ProductSummary(BaseModel):
id: int
name: str
price: float

class PaginatedResponse(BaseModel, Generic[T]):
items: List[T]
total: int
page: int
page_size: int
total_pages: int

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}")
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: 2
Hints

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.


#11Full API Request/Response Model with Strict ValidationHard
pydanticAPI modelstrictmodel_validatorfield_validatorresponse

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'

class CreateTaskRequest(BaseModel):
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)

@field_validator('title')
@classmethod
def strip_title(cls, value):
value = value.strip()
if not value:
raise ValueError('Title cannot be empty or whitespace only')
return value

@field_validator('tags')
@classmethod
def normalize_tags(cls, value):
if len(value) > 10:
raise ValueError('Cannot have more than 10 tags')
lowered = [t.lower() for t in value]
return list(dict.fromkeys(lowered))

@field_validator('assigned_to')
@classmethod
def validate_assignees(cls, value):
if len(value) > 5:
raise ValueError('Cannot assign more than 5 users')
if any(uid <= 0 for uid in value):
raise ValueError('All user IDs must be positive integers')
return value

@model_validator(mode='after')
def validate_due_date(self):
if self.due_date is not None and self.due_date < datetime.utcnow():
raise ValueError(f"due_date must be in the future, got {self.due_date}")
return self

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'

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}")

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()}")

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()}")

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}")
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 bug
Hints

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.

© 2026 EngineersOfAI. All rights reserved.