Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Data validation and settings management using Python type annotations with Pydantic v2
.claude/skills/aiskillstore-pydantic/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 137% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 148% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 76% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 152% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 144% | 0% |
Pydantic is a data validation library that uses Python type annotations to define data schemas, offering fast and extensible validation with automatic type coercion.
pythonfrom pydantic import BaseModel from datetime import datetime from typing import Optional class User(BaseModel): id: int name: str email: str signup_ts: Optional[datetime] = None is_active: bool = True # Automatic type coercion user = User( id='123', # String → int name='John Doe', email='john@example.com', signup_ts='2017-06-01 12:22' # String → datetime )
python# From dict user = User.model_validate({'id': 1, 'name': 'Alice', 'email': 'alice@test.com'}) # From JSON user = User.model_validate_json('{"id": 1, "name": "Alice", "email": "alice@test.com"}') # Serialization print(user.model_dump()) # Python dict print(user.model_dump_json()) # JSON string
pythonfrom pydantic import BaseModel, Field, EmailStr, HttpUrl from typing import Annotated class Product(BaseModel): product_id: int = Field(alias='id', ge=1, description='Unique product identifier') name: str = Field(min_length=1, max_length=200) price: float = Field(gt=0, le=1000000) email: EmailStr website: HttpUrl tags: list[str] = Field(default_factory=list, max_length=10) internal_code: str = Field(exclude=True, default='N/A') class User(BaseModel): username: Annotated[str, Field(min_length=3, pattern=r'^[a-zA-Z0-9_]+$')] age: int = Field(ge=0, le=150)
pythonfrom pydantic import BaseModel, ConfigDict class StrictModel(BaseModel): model_config = ConfigDict( strict=True, # No type coercion frozen=True, # Immutable instances validate_assignment=True, # Validate on attribute assignment extra='forbid', # Reject extra fields str_strip_whitespace=True, populate_by_name=True, # Accept both alias and field name use_enum_values=True, # Serialize enums as values ) id: int name: str
pythonfrom pydantic import BaseModel, model_validator, field_validator, ValidationError from typing import Any class DateRange(BaseModel): start_date: str end_date: str @field_validator('start_date', 'end_date') @classmethod def validate_date_format(cls, v: str) -> str: # Custom validation logic if not v: raise ValueError('Date cannot be empty') return v @model_validator(mode='after') def check_dates_order(self) -> 'DateRange': # Cross-field validation if self.start_date > self.end_date: raise ValueError('start_date must be before end_date') return self # Using the model try: date_range = DateRange(start_date='2024-01-01', end_date='2024-01-31') except ValidationError as e: for error in e.errors(): print(f"{error['loc']}: {error['msg']}")
pythonfrom pydantic import BaseModel, Field, SecretStr from datetime import datetime class User(BaseModel): id: int username: str password: SecretStr created_at: datetime internal_data: dict = Field(exclude=True, default_factory=dict) # Serialization options user = User( id=1, username='john', password='secret', created_at=datetime.now() ) # Basic serialization print(user.model_dump()) # Python dict print(user.model_dump_json()) # JSON string # Excluding fields print(user.model_dump(exclude={'password'})) print(user.model_dump(exclude={'username', 'created_at'})) # Include only specific fields print(user.model_dump(include={'id', 'username'})) # JSON-compatible serialization print(user.model_dump(mode='json')) # datetime → string print(user.model_dump(by_alias=True)) # Use field aliases
pythonfrom typing import Annotated, Any from pydantic import BaseModel, field_serializer, PlainSerializer class Model(BaseModel): number: int created_at: datetime @field_serializer('number') def serialize_number(self, value: int) -> str: return f"{value:,}" # Format with commas # Using Annotated with PlainSerializer custom_field: Annotated[ float, PlainSerializer(lambda x: round(x, 2), return_type=float) ]
pythonfrom pydantic import BaseModel from typing import Optional, List class Address(BaseModel): street: str city: str country: str = 'USA' zip_code: str class User(BaseModel): id: int name: str addresses: List[Address] primary_address: Optional[Address] = None # Usage user = User( id=1, name='John Doe', addresses=[ {'street': '123 Main St', 'city': 'New York', 'zip_code': '10001'}, {'street': '456 Oak Ave', 'city': 'Boston', 'zip_code': '02101'} ], primary_address={'street': '123 Main St', 'city': 'New York', 'zip_code': '10001'} )
pythonfrom enum import Enum, IntEnum from pydantic import BaseModel class Status(str, Enum): PENDING = 'pending' ACTIVE = 'active' COMPLETED = 'completed' class Priority(IntEnum): LOW = 1 MEDIUM = 2 HIGH = 3 class Task(BaseModel): title: str status: Status = Status.PENDING priority: Priority = Priority.MEDIUM model_config = ConfigDict(use_enum_values=True) # Can use enum values or names task1 = Task(title='Task 1', status='active', priority=3) task2 = Task(title='Task 2', status=Status.ACTIVE, priority=Priority.HIGH)
pythonfrom pydantic import TypeAdapter from typing import List, Optional # Validate individual types without full models int_adapter = TypeAdapter(int) print(int_adapter.validate_python('123')) # 123 list_adapter = TypeAdapter(List[int]) print(list_adapter.validate_python(['1', '2', '3'])) # [1, 2, 3] # Generate JSON schemas print(int_adapter.json_schema()) print(list_adapter.json_schema())
pythonfrom pydantic import BaseModel, ValidationError from typing import Union class EmailValidator(BaseModel): email: str @field_validator('email') @classmethod def validate_email(cls, v: str) -> str: if '@' not in v: raise ValueError('Invalid email format') return v.lower() # Validation error handling try: user = User(id='invalid', name='', email='test') except ValidationError as e: print(f"Errors: {e.error_count()}") for error in e.errors(): print(f" {error['loc']}: {error['msg']} ({error['type']})")
uv add pydanticuv add pydantic[email] for EmailStruv add pydantic[url] for HttpUrluv add pydantic[typing-extensions] for extended type supportconint(gt=0) over int for positive numbersConfigDict to set global model behavior@field_validatormodel_dump() parameters to control output format| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 7,266 | 11,561 | +59% | 1 | 1 | 0% | 1,345 | 3,338 | +148% | 0 | 0 | — |
case-02 | fail→pass | 5,878 | 9,504 | +62% | 1 | 1 | 0% | 1,309 | 3,103 | +137% | 0 | 0 | — |
case-03 | pass→pass | 11,639 | 7,769 | -33% | 1 | 1 | 0% | 2,132 | 3,748 | +76% | 0 | 0 | — |
case-04 | pass→pass | 7,388 | 6,960 | -6% | 1 | 1 | 0% | 1,374 | 3,456 | +152% | 0 | 0 | — |
case-05 | pass→pass | 11,616 | 9,223 | -21% | 1 | 1 | 0% | 1,207 | 2,950 | +144% | 0 | 0 | — |
case-06 | pass→pass | 6,079 | 9,006 | +48% | 1 | 1 | 0% | 894 | 2,997 | +235% | 0 | 0 | — |
case-07 | pass→pass | 10,083 | 7,998 | -21% | 1 | 1 | 0% | 923 | 2,833 | +207% | 0 | 0 | — |
case-08 | pass→pass | 9,487 | 2,937 | -69% | 1 | 1 | 0% | 762 | 2,750 | +261% | 0 | 0 | — |
case-09 | pass→pass | 3,999 | 4,739 | +19% | 1 | 1 | 0% | 763 | 3,068 | +302% | 0 | 0 | — |
case-10 | pass→pass | 14,520 | 7,490 | -48% | 1 | 1 | 0% | 1,742 | 3,707 | +113% | 0 | 0 | — |
case-11 | pass→pass | 13,175 | 10,109 | -23% | 1 | 1 | 0% | 1,491 | 3,179 | +113% | 0 | 0 | — |
case-12 | pass→pass | 9,862 | 6,042 | -39% | 1 | 1 | 0% | 1,728 | 3,211 | +86% | 0 | 0 | — |
case-13 | pass→pass | 13,959 | 9,883 | -29% | 1 | 1 | 0% | 1,671 | 3,086 | +85% | 0 | 0 | — |
case-14 | pass→pass | 6,692 | 11,945 | +78% | 1 | 1 | 0% | 1,211 | 3,512 | +190% | 0 | 0 | — |
case-15 | pass→pass | 13,022 | 10,356 | -20% | 1 | 1 | 0% | 2,559 | 4,234 | +65% | 0 | 0 | — |
case-16 | pass→pass | 13,748 | 10,947 | -20% | 1 | 1 | 0% | 1,699 | 3,336 | +96% | 0 | 0 | — |
case-17 | pass→pass | 10,501 | 7,799 | -26% | 1 | 1 | 0% | 1,950 | 3,696 | +90% | 0 | 0 | — |
case-18 | pass→pass | 14,612 | 4,254 | -71% | 1 | 1 | 0% | 1,437 | 3,031 | +111% | 0 | 0 | — |
case-19 | pass→pass | 9,848 | 10,371 | +5% | 1 | 1 | 0% | 1,685 | 3,103 | +84% | 0 | 0 | — |
case-20 | pass→pass | 35,208 | 26,379 | -25% | 1 | 1 | 0% | 6,013 | 4,616 | -23% | 0 | 0 | — |
case-21 | pass→pass | 9,799 | 3,918 | -60% | 1 | 1 | 0% | 857 | 2,897 | +238% | 0 | 0 | — |
case-22 | pass→pass | 17,621 | 9,546 | -46% | 1 | 1 | 0% | 1,137 | 2,935 | +158% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.