Skip to main content
rule

pydantic — Cursor rule: what it does, how to install

Instructs Cursor to generate type-safe Pydantic v2 models. It prevents mutable default bugs and routes environment configs through pydantic_settings.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/pydantic.mdc
Open on GitHub

What pydantic does

The rule applies across all **/*.py files in your repository. It enforces singular class names inside a dedicated models/ folder and restricts untyped Any fields in favor of StrictInt or StrictFloat. Models default to immutability via ConfigDict(frozen=True) or validate updates through validate_assignment=True.

Mutable collection defaults like empty lists or dictionaries are replaced with Field(default_factory=...). For validation, Cursor applies @field_validator and @model_validator(mode='after') while dropping deprecated patterns like __post_init__. Application secrets and environment values stay isolated inside BaseSettings classes using SecretStr.

This guide targets Pydantic v2. Projects still relying on v1 will immediately fail on imports such as ConfigDict and model_validator.

What it helps with

  • structure schemas inside a dedicated models/ package
  • prevent shared mutable states by using Field(default_factory=...)
  • validate interdependent attributes using @model_validator
  • manage environment variables with BaseSettings and SecretStr

Example prompts for the agent

  • Create an immutable event model with positive duration validation
  • Set up an application settings class reading from .env
  • Refactor this legacy dictionary schema into a strict Pydantic v2 model

How to install pydantic

Manually: put the files in place

.cursor/rules/pydantic.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about pydantic

How to install pydantic in Cursor?

Place the rule file at .cursor/rules/pydantic.mdc in your project root. Cursor will automatically apply these instructions to all Python files matching **/*.py.

What does the pydantic rule do?

It establishes coding standards for Pydantic v2 models. The rule enforces strict types, safe default factories, and clean configuration management.

Does this rule support Pydantic v1?

No, it specifically targets Pydantic v2 best practices. Generated code relies on ConfigDict, field_validator, and the standalone pydantic_settings package.

Source text

pydantic.mdc · CC0-1.0
---
description: Definitive guidelines for writing robust, maintainable, and performant pydantic models in Python, focusing on v2.12.5 best practices.
globs: **/*.py
---
# pydantic Best Practices

Pydantic v2 is the standard for data validation in modern Python. Follow these rules to build type-safe, efficient, and maintainable data models.

## 1. Model Naming and Organization

**Always use clear, singular nouns for models and group them in a dedicated `models/` package.** This improves discoverability and maintains a consistent project structure.

❌ **BAD:**
```python
# In user_operations.py
class UserData(BaseModel):
    name: str
    email: EmailStr

# In users_api.py
class UserRequest(BaseModel):
    name: str
    email: EmailStr
```

✅ **GOOD:**
```python
# In models/user.py
from pydantic import BaseModel, EmailStr

class User(BaseModel):
    id: int
    name: str
    email: EmailStr
```

## 2. Strict Typing and Immutability

**Prefer concrete types over `Any`. Use `Strict*` types when no coercion is acceptable. Enable `validate_assignment=True` for mutable models, or `frozen=True` for immutable models.** Immutability is generally preferred for data models to prevent unexpected state changes.

❌ **BAD:**
```python
from pydantic import BaseModel
from typing import Any

class Item(BaseModel):
    quantity: Any # Allows "5" or 5
    price: float # Allows "10.5" or 10.5
```

✅ **GOOD:**
```python
from pydantic import BaseModel, StrictInt, StrictFloat, ConfigDict

class ImmutableItem(BaseModel):
    model_config = ConfigDict(frozen=True) # Makes instances immutable
    id: int
    name: str
    quantity: StrictInt # Only accepts int, "5" will fail
    price: StrictFloat # Only accepts float, "10.5" will fail

class MutableUser(BaseModel):
    model_config = ConfigDict(validate_assignment=True) # Validates on update
    name: str
    age: int

user = MutableUser(name="Alice", age=30)
user.age = "31" # This will raise a ValidationError
```

## 3. Safe Default Values

**Never use mutable objects (lists, dicts, sets) as direct default values.** This leads to shared state across instances. Always use `default_factory` or `Field(default_factory=...)`.

❌ **BAD:**
```python
from pydantic import BaseModel

class Product(BaseModel):
    name: str
    tags: list = [] # Shared list across all Product instances
```

✅ **GOOD:**
```python
from pydantic import BaseModel, Field

class Product(BaseModel):
    name: str
    tags: list[str] = Field(default_factory=list) # Each instance gets a new list
```

## 4. Custom Validation Logic

**Use `@field_validator` for single-field validation and `@model_validator` for cross-field validation.** Keep validation logic concise and focused within the model definition. Extract complex business logic to separate service layers.

❌ **BAD:**
```python
from pydantic import BaseModel, ValidationError

class Event(BaseModel):
    start_time: int
    end_time: int

    def __post_init__(self): # Not a Pydantic v2 pattern
        if self.start_time >= self.end_time:
            raise ValueError("Start time must be before end time")
```

✅ **GOOD:**
```python
from pydantic import BaseModel, ValidationError, field_validator, model_validator
from typing import Self

class Event(BaseModel):
    start_time: int
    end_time: int

    @field_validator('start_time')
    @classmethod
    def check_start_time_positive(cls, v: int) -> int:
        if v < 0:
            raise ValueError("Start time must be positive")
        return v

    @model_validator(mode='after')
    def check_time_order(self) -> Self:
        if self.start_time >= self.end_time:
            raise ValueError("Start time must be before end time")
        return self

try:
    Event(start_time=10, end_time=5)
except ValidationError as e:
    print(e) # Shows 'Start time must be before end time'
```

## 5. Settings Management

**Store application configuration in `pydantic_settings.BaseSettings` subclasses.** This automatically validates environment variables and ensures secrets are handled securely (e.g., with `SecretStr`).

❌ **BAD:**
```python
# config.py
API_KEY = os.getenv("MY_API_KEY", "default_secret")
DATABASE_URL = "sqlite:///./test.db"
```

✅ **GOOD:**
```python
# settings.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import SecretStr

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file='.env', extra='ignore') # Load from .env, ignore unknown env vars

    api_key: SecretStr
    database_url: str = "sqlite:///./test.db"

# Usage:
settings = Settings()
print(settings.api_key.get_secret_value()) # Access secret value safely
```

## 6. Editor Integration (VS Code / Pylance)

**Configure VS Code with Pylance (`Type Checking Mode: strict`) to get real-time autocompletion and type error diagnostics.** For intentional Pydantic coercions that Pylance flags, use specific ignore comments or `cast`.

❌ **BAD:**
```python
# No Pylance, or Pylance in 'off' mode. Misses errors early.
# Or, using general # type: ignore for all lines with Pydantic coercion.
from pydantic import BaseModel
class User(BaseModel):
    age: int
user = User(age='23') # No error shown, but Pylance could warn
```

✅ **GOOD:**
```python
# VS Code settings: "python.analysis.typeCheckingMode": "strict"
from pydantic import BaseModel
from typing import cast

class User(BaseModel):
    age: int

user_str_age = User(age='23') # Pylance will flag this as str -> int, which Pydantic handles.
# If you want to silence Pylance for this specific, known coercion:
user_str_age_ignored = User(age='23') # pyright: ignore[reportGeneralTypeIssues]
# Or explicitly cast if you prefer:
user_str_age_casted = User(age=cast(int, '23'))
```

## 7. Common Pitfalls

**Avoid field names that collide with Python keywords or Pydantic internal attributes.** This can lead to unexpected behavior or validation errors.

❌ **BAD:**
```python
from pydantic import BaseModel
from typing import Optional

class BadModel(BaseModel):
    int: Optional[int] = None # 'int' is a built-in type, causes collision
    model_config: str = "some_config" # Collides with Pydantic's model_config
```

✅ **GOOD:**
```python
from pydantic import BaseModel
from typing import Optional

class GoodModel(BaseModel):
    integer_value: Optional[int] = None
    custom_config_name: str = "some_config"
```

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

All IDE rules for Backend and APIs →

From the same set240

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor