Pydantic Cursor Rules: Python Data Validation
Cursor rules for Pydantic covering BaseModel, field types/validators, nested models, ConfigDict, JSON Schema, BaseSettings, discriminated unions, and ORM integration.

Overview
Pydantic is the standard Python library for data validation and settings management, using type hints to define schemas with runtime validation, JSON Schema generation, and serialization. These cursor rules enforce BaseModel conventions, field validators, nested model patterns, ConfigDict settings, BaseSettings for environment variables, and FastAPI integration so AI assistants generate type-safe, validated Python models.
Note:
Enforces BaseModel with explicit type annotations, @field_validator/@model_validator patterns, ConfigDict (frozen, extra, from_attributes), BaseSettings for env vars, discriminated unions, and model_dump/model_validate_json patterns.
Rules Configuration
---
description: Enforces Pydantic best practices including BaseModel conventions, field validators, nested models, ConfigDict, BaseSettings, discriminated unions, and FastAPI integration patterns.
globs: **/*.py,**/models/**/*.py,**/schemas/**/*.py
---
# Pydantic Best Practices
You are an expert in Pydantic, Python type systems, and data validation.
You understand type annotations, validation pipelines, JSON Schema generation, and FastAPI integration.
### BaseModel Conventions
- Inherit from `BaseModel`: `class User(BaseModel)`
- Use type annotations for all fields: `name: str`, `age: int`, `email: EmailStr`
- Use `Field()` for extra metadata: `Field(default=..., description="...", gt=0, lt=150)`
- Required fields: no default value. Optional: `Optional[str] = None` or `str = "default"`
- Use `model_config = ConfigDict(...)` for model-level configuration
- Prefer `model_dump()` over `.dict()` (V1 → V2 migration)
- Prefer `model_validate()` over `.parse_obj()` (V1 → V2 migration)
### Field Types & Customization
- Standard types: `str`, `int`, `float`, `bool`, `bytes`, `datetime`, `date`, `UUID`
- Specialized types: `EmailStr`, `HttpUrl`, `IPvAnyAddress`, `FilePath`, `DirectoryPath`
- Constrained types: `conint(gt=0, lt=100)`, `constr(min_length=1, max_length=255)`
- Use `Annotated` for reusable constraints: `PositiveInt = Annotated[int, Field(gt=0)]`
- Use `Enum` for constrained choices: `class Status(str, Enum): ACTIVE = "active"`
- Use `Literal["option1", "option2"]` for string unions
- Optional with default: `Optional[str] = None` — never `str = None` alone
### Validators
- Use `@field_validator("field_name")` for single-field validation
- Field validators run before model validators
- Use `@model_validator(mode="before")` to pre-process input data
- Use `@model_validator(mode="after")` for cross-field validation
- Validators must return the value (or raise ValueError/AssertionError)
- Use `info: ValidationInfo` for context: `info.data` (other validated fields)
- `@field_validator("field", mode="before")` runs before type coercion
- `@field_validator("field", mode="after")` runs after type coercion (default)
### Nested Models
- Use Pydantic models as field types: `address: Address`
- Lists of models: `items: list[Item]`
- Nested models auto-validate during parent model validation
- JSON Schema generated recursively for nested models
- Self-referencing models: use `from __future__ import annotations` or string forward references
- Call `model_rebuild()` after defining all referenced models
### Configuration (ConfigDict)
- `frozen=True` — immutable model instances (useful for config objects)
- `extra="forbid"` — reject unknown fields (good for API input validation)
- `extra="ignore"` — silently drop unknown fields (default)
- `from_attributes=True` — allow validation from ORM objects (was `orm_mode`)
- `str_strip_whitespace=True` — auto-strip string fields
- `use_enum_values=True` — serialize enums as their values
- `json_schema_extra={...}` — custom JSON Schema additions
- `validate_default=True` — validate default values against field types
### Serialization
- `model_dump()` → Python dict (was `.dict()`)
- `model_dump_json()` → JSON string (was `.json()`)
- `model_dump(exclude={"password"})` — exclude sensitive fields
- `model_dump(include={"id", "name"})` — include only specific fields
- `model_dump(mode="json")` — JSON-safe types (datetime → ISO string)
- `model_dump(exclude_unset=True)` — only fields explicitly set
- Custom serializers: `@field_serializer("field_name")`
### BaseSettings
- Inherit from `BaseSettings` for environment variable config
- Fields read from env vars (case-insensitive) by default
- Use `Field(alias="DB_HOST")` for different env var names
- `model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")`
- Use `env_prefix="APP_"` to namespace: `APP_DATABASE_URL` → `database_url`
- Secrets: `secret_key: SecretStr` — masks value in repr and log output
### FastAPI Integration
- Use Pydantic models for request body validation: `async def create(user: UserCreate) -> User`
- Use for query parameters: `async def list(limit: int = Query(default=10))`
- Use response_model for output shaping: `@app.post("/users", response_model=UserResponse)`
- Use discriminated unions for polymorphic responses: `response_model=Annotated[Union[A, B], Field(discriminator="type")]`
- Return models from endpoints — FastAPI handles serialization
- Generate OpenAPI schema automatically from Pydantic models
### Discriminated Unions
- Use `Literal` as discriminator: `class Cat(BaseModel): type: Literal["cat"] = "cat"; lives: int`
- Annotate union: `Pet = Annotated[Union[Cat, Dog], Field(discriminator="type")]`
- Each variant must have the discriminator field with a unique literal value
- FastAPI uses discriminated unions for OpenAPI oneOf with discriminator
### Dynamic Models
- Use `create_model("Name", field1=(str, ...), field2=(int, 0))` for runtime model creation
- Provide `__base__=ExistingModel` to extend existing models
- Use for programmatic schema generation from database metadata
- Dynamic models support full validation, serialization, and JSON Schema like static models
Installation
Create pydantic.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.
pip install pydantic
# With email validation
pip install "pydantic[email]"
# With settings management
pip install pydantic-settings
Examples
# models.py — BaseModel with validators and nested models
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
class Address(BaseModel):
street: str
city: str
country: str = Field(min_length=2, max_length=2)
zip_code: str
class User(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
id: int
name: str = Field(min_length=1, max_length=100)
email: EmailStr
age: int = Field(gt=0, lt=150)
address: Address
tags: list[str] = []
created_at: datetime = Field(default_factory=datetime.now)
password: Optional[str] = Field(default=None, exclude=True)
@field_validator("name")
@classmethod
def name_must_be_title_case(cls, v: str) -> str:
if not v.istitle():
raise ValueError("Name must be in title case")
return v
@field_validator("tags")
@classmethod
def tags_must_be_lowercase(cls, v: list[str]) -> list[str]:
return [tag.lower().strip() for tag in v]
# Usage
user = User(
id=1,
name="Alice Smith",
email="[email protected]",
age=30,
address={"street": "123 Main", "city": "NYC", "country": "US", "zip_code": "10001"},
tags=[" Python ", "Data"],
)
print(user.model_dump(exclude={"password"}))
# settings.py — BaseSettings for environment configuration
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="APP_",
)
database_url: str
redis_url: str = "redis://localhost:6379"
debug: bool = False
max_connections: int = Field(default=10, gt=0)
settings = Settings()
# api.py — FastAPI with discriminated unions
from typing import Annotated, Literal, Union
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class TextContent(BaseModel):
type: Literal["text"] = "text"
body: str
class ImageContent(BaseModel):
type: Literal["image"] = "image"
url: str
alt: str
Message = Annotated[Union[TextContent, ImageContent], Field(discriminator="type")]
class CreateMessage(BaseModel):
content: Message
@app.post("/messages", response_model=CreateMessage)
async def create_message(msg: CreateMessage):
return msg
Related Resources
Related Articles
React Native Cursor Rules: Mobile Best Practices Guide
Professional React Native cursor rules guiding component architecture, TypeScript, navigation, testing with Jest and Detox, and performance optimization for cross‑platform iOS and Android apps.
Bun Cursor Rules: All-in-One JavaScript Toolkit
Cursor rules for Bun covering the runtime, test runner, package manager, bundler, Bun.serve HTTP server, built-in SQLite, S3 client, HTMLRewriter, and native TypeScript/JSX support.
Flutter Cursor Rules: Widgets, State Management, Performance
Flutter cursor rules for clean widgets, state management, performance, platform integration, testing, and deployment. Build maintainable, reliable mobile apps.