Raw view: Pydantic
No invisible, zero-width, or bidi-control characters detected.
---
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"
```