Pydantic: BaseModel, валидаторы, Settings
Pydantic это библиотека для data validation на основе type hints. Используется в FastAPI для валидации request/response, в settings management, для парсинга API-ответов, JSON config. В этом уроке - BaseModel, custom validators, Settings из env переменных, и почему Pydantic стал стандартом в Python backend.
Установка
pip install pydantic
pip install pydantic-settings # для Settings (отдельный пакет)
В Python 2026 используется Pydantic v2 - переписан с компилируемым ядром для скорости (10-50x быстрее v1). API частично отличается от v1.
BaseModel - основа
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
email: str
u = User(name="Alice", age=30, email="a@b.c")
print(u.name) # Alice
print(u.model_dump()) # {'name': 'Alice', 'age': 30, 'email': 'a@b.c'}
# Из JSON
u = User.model_validate_json('{"name": "Bob", "age": 25, "email": "b@b.c"}')
# В JSON
json_str = u.model_dump_json()
В отличие от dataclass, Pydantic валидирует при создании. Если передать невалидный тип, бросится ValidationError.
Type coercion
Pydantic пытается конвертировать совместимые типы:
u = User(name="Alice", age="30", email="a@b.c") # str "30" → int 30
print(type(u.age)) # int
# Невалидное
u = User(name="Alice", age="abc", email="a@b.c")
# ValidationError: Input should be a valid integer
Это удобно для парсинга JSON где числа могут приходить строками (например, query params).
В strict mode конверсии отключены - точное совпадение типа требуется. Регулируется на уровне модели или поля.
Optional поля и defaults
from pydantic import BaseModel
class User(BaseModel):
name: str # required
age: int = 18 # default
email: str | None = None # nullable
tags: list[str] = [] # mutable default - Pydantic создаёт копию
Pydantic правильно handles mutable defaults - не создаёт shared mutable. С dataclass нужен field(default_factory=list), с Pydantic просто пишешь [].
Validation rules через типы
from typing import Annotated
from pydantic import BaseModel, Field, EmailStr, conint
class User(BaseModel):
name: Annotated[str, Field(min_length=2, max_length=50)]
age: Annotated[int, Field(ge=0, le=150)]
email: EmailStr
score: Annotated[float, Field(ge=0.0, le=1.0)]
u = User(name="A", age=30, email="invalid", score=0.5)
# ValidationError:
# - name: ensure this value has at least 2 characters
# - email: value is not a valid email address
Field() для constraints. EmailStr, HttpUrl, IPvAnyAddress - готовые типы с встроенной валидацией. Для них нужен pip install email-validator или pydantic[email].
Custom validators
from pydantic import BaseModel, field_validator, model_validator
class User(BaseModel):
name: str
age: int
password: str
confirm_password: str
@field_validator("name")
@classmethod
def name_no_digits(cls, v: str) -> str:
if any(c.isdigit() for c in v):
raise ValueError("Name cannot contain digits")
return v.strip()
@field_validator("password")
@classmethod
def password_strength(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
return v
@model_validator(mode="after")
def passwords_match(self) -> "User":
if self.password != self.confirm_password:
raise ValueError("Passwords do not match")
return self
field_validator- валидация одного поляmodel_validator- валидация модели целиком (полезно для cross-field логики)mode="after"- после индивидуальных field validatorsmode="before"- до парсинга, получает raw input
Nested models
class Address(BaseModel):
street: str
city: str
country: str
class User(BaseModel):
name: str
address: Address
backup_addresses: list[Address] = []
u = User(name="Alice", address={"street": "X", "city": "Y", "country": "Z"})
print(u.address.city)
Pydantic автоматически валидирует nested структуры. Можно передавать dict, Pydantic сам создаст вложенные модели.
Discriminated unions
Для variants одного объекта:
from typing import Literal
from pydantic import BaseModel, Field
class Cat(BaseModel):
pet_type: Literal["cat"]
meow_volume: int
class Dog(BaseModel):
pet_type: Literal["dog"]
bark_loudness: int
class Owner(BaseModel):
name: str
pet: Cat | Dog = Field(discriminator="pet_type")
owner = Owner(name="Alice", pet={"pet_type": "cat", "meow_volume": 5})
print(type(owner.pet)) # Cat
discriminator определяет какой тип используется по значению поля. Это эффективнее чем try-each-type подход.
Pydantic Settings
Конфигурация из env переменных (руками их читают через os.environ - см. урок про os и pathlib):
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "My App"
database_url: str
debug: bool = False
api_keys: list[str] = []
class Config:
env_file = ".env"
env_prefix = "MYAPP_"
case_sensitive = False
settings = Settings()
# Читает env: MYAPP_APP_NAME, MYAPP_DATABASE_URL, MYAPP_DEBUG, MYAPP_API_KEYS
С .env файлом:
MYAPP_DATABASE_URL=postgresql://localhost/mydb
MYAPP_DEBUG=true
MYAPP_API_KEYS=["key1", "key2"]
Стандартный паттерн в production:
- Defaults в коде
- Override через env vars (12-factor app)
- Локальная разработка через
.envфайл (в.gitignore)
SecretStr - защита секретов
from pydantic import SecretStr
class Settings(BaseSettings):
api_key: SecretStr
s = Settings(api_key="my-secret-key")
print(s.api_key) # SecretStr('**********') - safe для логов
print(s.api_key.get_secret_value()) # "my-secret-key" - реальное значение
SecretStr скрывает значение в repr/str - защита от случайной утечки в логах.
Сериализация
class User(BaseModel):
name: str
age: int
password: str
u = User(name="Alice", age=30, password="secret")
# В dict
u.model_dump()
# {'name': 'Alice', 'age': 30, 'password': 'secret'}
# В JSON string
u.model_dump_json()
# Исключить поля
u.model_dump(exclude={"password"})
# {'name': 'Alice', 'age': 30}
# Только конкретные
u.model_dump(include={"name"})
# {'name': 'Alice'}
# Только set поля (не defaults)
u2 = User(name="Bob", age=25, password="x")
u2.model_dump(exclude_unset=True)
# зависит от того, что было передано в init
Alias - другое имя в JSON
from pydantic import BaseModel, Field
class User(BaseModel):
full_name: str = Field(alias="fullName")
u = User.model_validate({"fullName": "Alice Smith"})
print(u.full_name) # 'Alice Smith'
# При сериализации тоже camelCase
u.model_dump(by_alias=True)
# {'fullName': 'Alice Smith'}
Полезно для API с camelCase JSON и Python snake_case кодом.
Config model behavior
from pydantic import BaseModel, ConfigDict
class StrictUser(BaseModel):
model_config = ConfigDict(
strict=True, # без type coercion
extra="forbid", # ошибка на дополнительные поля
frozen=True, # immutable после создания
populate_by_name=True, # accept as snake_case и alias
str_strip_whitespace=True, # trim strings
validate_assignment=True, # валидация при obj.field = x
)
name: str
extra="forbid" критично для security - предотвращает unexpected fields в request bodies. strict=True для жёсткой типизации без surprises.
Pydantic для парсинга API-ответов
import httpx
from pydantic import BaseModel
class GithubUser(BaseModel):
login: str
id: int
name: str | None = None
followers: int
async def fetch_user(username: str) -> GithubUser:
async with httpx.AsyncClient() as client:
r = await client.get(f"https://api.github.com/users/{username}")
r.raise_for_status()
return GithubUser.model_validate(r.json())
user = await fetch_user("alice")
print(user.followers) # типизированный доступ
Внешний API возвращает JSON, Pydantic валидирует и даёт типизированный объект. Если API возвращает что-то неожиданное - явная ошибка, не silent dict access.
Pydantic vs dataclass
| Feature | dataclass | Pydantic |
|---|---|---|
| Generates init | Да | Да |
| Type checking | Только через mypy | Runtime валидация |
| Defaults | Через field() для mutable | Прямо |
| Custom validators | Через post_init | @field_validator, @model_validator |
| JSON serialization | Через external (asdict + json) | model_dump_json() |
| Performance | Чуть быстрее (без validation) | Быстрее в v2 (compiled core) |
| Use case | Простые data containers | API/config validation |
Для backend часто Pydantic из-за валидации. Для internal data containers - dataclass.
ORM/dict mode
class User(BaseModel):
name: str
age: int
model_config = ConfigDict(from_attributes=True)
# Из ORM-like объекта
class SqlAlchemyUser:
name = "Alice"
age = 30
user = User.model_validate(SqlAlchemyUser())
from_attributes=True позволяет создавать Pydantic из объектов с атрибутами (SQLAlchemy ORM, дataclass, любых). Полезно для conversion между БД и API layers.
Распространённые ошибки
1. Mutable default
class Bad(BaseModel):
tags: list = [] # OK в Pydantic - сам обрабатывает
В dataclass это бы было ошибкой, в Pydantic - валидно. Каждая инстанция получает свой список.
2. Использование dict вместо BaseModel
# Плохо в FastAPI
@app.post("/users")
def create(data: dict):
return data # нет валидации
Используй Pydantic models для всех body. Это и валидация, и documentation, и type safety.
3. extra="allow" в API
class User(BaseModel):
name: str
model_config = ConfigDict(extra="allow") # ОПАСНО
# Принимает любые поля - utечка данных, surprising behavior
Для API лучше extra="forbid" (или default "ignore") - explicit лучше implicit.
4. SecretStr в обычных полях
class User(BaseModel):
name: SecretStr # ОВЕРКИЛЛ
SecretStr для секретов (passwords, API keys). Для обычных полей - обычный str. SecretStr усложняет работу с полем (нужен get_secret_value()).
5. Игнорирование ValidationError
try:
user = User(...)
except Exception: # слишком широко
pass
Catch конкретный ValidationError, обработай каждое поле через exc.errors() для нормального error response.
Hooks через model_validator
Pre-processing data перед валидацией:
from pydantic import model_validator
class User(BaseModel):
name: str
email: str
@model_validator(mode="before")
@classmethod
def normalize(cls, data: dict) -> dict:
if isinstance(data, dict):
if "email" in data:
data["email"] = data["email"].lower().strip()
if "name" in data:
data["name"] = data["name"].strip()
return data
mode="before" обрабатывает raw data до валидации - удобно для normalization (trim, lowercase, etc).
Сравнение с Go и PHP
В Go используют encoding/json со struct tags:
type User struct {
Name string `json:"name" validate:"required,min=2"`
Age int `json:"age" validate:"min=0,max=150"`
Email string `json:"email" validate:"email"`
}
// Парсинг
var user User
json.Unmarshal(data, &user)
// Валидация через библиотеку (например, go-playground/validator)
validate.Struct(user)
В PHP с Symfony Validator или JsonSerializable:
class User {
#[Assert\NotBlank]
#[Assert\Length(min: 2)]
public string $name;
#[Assert\Range(min: 0, max: 150)]
public int $age;
}
Pydantic объединяет parsing + validation в одном API через type hints. Это elegantly: один класс описывает и схему, и правила. Декларативный подход.
Pydantic в asyncio контексте
Pydantic v2 thread-safe и async-friendly. Можно использовать в async коде без проблем:
async def process(data: dict) -> User:
user = User.model_validate(data) # синхронная operation
await save_to_db(user)
return user
Сама валидация синхронная (CPU-bound), но это OK - она быстрая. В async handler никаких проблем.
Мини-задание
- Базовая модель с валидацией:
from pydantic import BaseModel, Field, field_validator
class Product(BaseModel):
name: str = Field(min_length=1, max_length=100)
price: float = Field(ge=0)
in_stock: bool = True
@field_validator("name")
@classmethod
def name_capitalize(cls, v: str) -> str:
return v.strip().capitalize()
p = Product(name="apple", price=99.99)
print(p.name) # Apple
print(p.model_dump_json(indent=2))
- Nested и custom valid:
from pydantic import BaseModel, EmailStr
class Address(BaseModel):
city: str
zip_code: str = Field(pattern=r"^\d{6}quot;)
class User(BaseModel):
name: str
email: EmailStr
addresses: list[Address] = []
user = User(
name="Alice",
email="alice@example.com",
addresses=[
{"city": "Moscow", "zip_code": "101000"},
{"city": "London", "zip_code": "200000"},
],
)
print(user.model_dump())
- Settings из env:
from pydantic_settings import BaseSettings
import os
os.environ["APP_DATABASE_URL"] = "postgresql://localhost/test"
os.environ["APP_DEBUG"] = "true"
class Settings(BaseSettings):
database_url: str
debug: bool = False
max_workers: int = 4
class Config:
env_prefix = "APP_"
settings = Settings()
print(settings.database_url) # postgresql://localhost/test
print(settings.debug) # True
print(settings.max_workers) # 4 (default)
Что дальше
Освоили Pydantic. В следующем уроке - SQLAlchemy: ORM для работы с реляционными БД. Это слой персистентности нашего backend.