Pydantic: валидация данных в Python
Что такое Pydantic и как работать с v2: BaseModel, Field, field_validator, model_dump и model_validate, ValidationError, pydantic-settings и переход с v1.
Содержание · 13 разделов
- Установка
- BaseModel: модели и типы
- Ограничения полей: Field
- ValidationError: как читать ошибки
- Свои валидаторы: field_validator и model_validator
- Сериализация: model_dump и model_validate
- Настройки модели: ConfigDict
- pydantic-settings: настройки из переменных окружения
- Pydantic в FastAPI
- Отличия Pydantic v1 и v2
- Частые ошибки
- PydanticImportError: BaseSettings has been moved
- Поле Optional стало обязательным
- Валидатор ничего не возвращает
- Лишние поля молча пропадают
- ModuleNotFoundError: No module named ‘pydantic_settings’
- Частые вопросы
- Чем Pydantic отличается от dataclasses?
- Как принимать JSON с ключами в camelCase?
- Как проверить список объектов без модели-обёртки?
- Как добавить в вывод вычисляемое поле?
- Итог
Pydantic — библиотека Python для валидации данных на основе аннотаций типов. Вы описываете структуру данных классом-наследником BaseModel, а Pydantic проверяет входные значения, приводит их к нужным типам и выдаёт подробную ошибку, если данные не подходят:
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
User(id="42", name="Анна") # → id=42 name='Анна' (строка "42" стала числом)
На Pydantic построена валидация в FastAPI, его используют для настроек приложений, разбора JSON из внешних API и конфигов. В статье разберём Pydantic v2 — актуальную ветку библиотеки: модели и типы, Field, собственные валидаторы, сериализацию, обработку ошибок, pydantic-settings и отличия от v1. Примеры проверены на Pydantic 2.14 и Python 3.12.
Установка
pip install pydantic
Если вы установили FastAPI (pip install "fastapi[standard]"), Pydantic уже есть — это обязательная зависимость фреймворка. Проверить версию:
python -c "import pydantic; print(pydantic.VERSION)"
# → 2.14.0
Устанавливайте пакеты в виртуальное окружение проекта.
BaseModel: модели и типы
Модель — это класс, поля которого описаны аннотациями типов. Поле со значением по умолчанию необязательное, без значения — обязательное:
from datetime import date
from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
birthday: date | None = None
tags: list[str] = []
user = User(id="42", name="Анна", email="anna@example.com", birthday="1995-03-08")
print(user)
# → id=42 name='Анна' email='anna@example.com' birthday=datetime.date(1995, 3, 8) tags=[]
print(user.id, type(user.id))
# → 42 <class 'int'>
Pydantic проверяет тип и приводит значение, если это безопасно: строка "42" превращается в int, строка "1995-03-08" — в datetime.date. Изменяемое значение по умолчанию вроде [] в Pydantic безопасно: каждый экземпляр получает свою копию списка.
Как работает приведение типов в режиме по умолчанию (lax):
| Поле | Вход | Результат |
|---|---|---|
int | "7" | 7 |
int | 1.0 | 1 |
int | 1.5 | ошибка: дробная часть |
str | 123 | ошибка: ожидается строка |
bool | "yes", "on", 1 | True |
bool | "да" | ошибка |
Чтобы отключить приведение и требовать точные типы, включите строгий режим: model_config = ConfigDict(strict=True). Тогда id="1" для поля int вызовет ошибку.
Кроме базовых типов Python Pydantic поддерживает datetime, UUID, Decimal, Enum, Literal, вложенные модели и специальные типы: EmailStr (нужен пакет email-validator), HttpUrl, SecretStr, PositiveInt и другие.
Ограничения полей: Field
Field задаёт значение по умолчанию и ограничения для поля:
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(min_length=2, max_length=100)
price: float = Field(gt=0, description="Цена в рублях")
quantity: int = Field(default=0, ge=0)
sku: str = Field(pattern=r"^[A-Z]{3}-\d{4}$")
print(Product(name="Чайник", price=1990, sku="KTL-0001"))
# → name='Чайник' price=1990.0 quantity=0 sku='KTL-0001'
Основные параметры Field:
| Параметр | Для чего |
|---|---|
default | значение по умолчанию |
default_factory | функция, создающая значение (list, dict, datetime.now) |
gt, ge, lt, le | больше, больше или равно, меньше, меньше или равно |
min_length, max_length | длина строки или списка |
pattern | регулярное выражение для строки |
alias | другое имя поля во входных данных |
description, examples | описание для JSON Schema и документации FastAPI |
exclude | не включать поле при сериализации |
ValidationError: как читать ошибки
Если данные не проходят проверку, Pydantic поднимает ValidationError и сообщает обо всех ошибках сразу:
from pydantic import ValidationError
try:
User(id="abc", name="Анна")
except ValidationError as e:
print(e)
2 validation errors for User
id
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='abc', input_type=str]
For further information visit https://errors.pydantic.dev/2.14/v/int_parsing
email
Field required [type=missing, input_value={'id': 'abc', 'name': 'Анна'}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.14/v/missing
Для программной обработки используйте e.errors() — список словарей. Обращайтесь к e внутри блока except: после выхода из него переменная удаляется.
try:
User(id="abc", name="Анна")
except ValidationError as e:
print(e.error_count())
print(e.errors()[0])
2
{'type': 'int_parsing', 'loc': ('id',), 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'abc', 'url': 'https://errors.pydantic.dev/2.14/v/int_parsing'}
Ссылка в url содержит номер вашей версии Pydantic. loc — путь к полю (для вложенных моделей он длиннее: ('address', 'city')), type — машинный код ошибки, msg — текст. Именно из этих данных FastAPI формирует ответ 422. О перехвате исключений в целом — в статье про try/except в Python.
Свои валидаторы: field_validator и model_validator
Когда встроенных ограничений мало, пишут валидаторы. @field_validator проверяет одно поле, @model_validator — модель целиком (например, сравнивает два поля):
from pydantic import BaseModel, ValidationError, field_validator, model_validator
class SignUp(BaseModel):
username: str
password: str
password_repeat: str
@field_validator("username")
@classmethod
def username_alnum(cls, value: str) -> str:
if not value.isalnum():
raise ValueError("только буквы и цифры")
return value.lower()
@model_validator(mode="after")
def passwords_match(self):
if self.password != self.password_repeat:
raise ValueError("пароли не совпадают")
return self
print(SignUp(username="Anna95", password="secret", password_repeat="secret"))
# → username='anna95' password='secret' password_repeat='secret'
Правила:
field_validator— метод класса (@classmethod), получает значение и возвращает его (можно изменённым: здесь имя приводится к нижнему регистру).- Ошибку сообщают через
raise ValueError(...)— Pydantic превратит её вValidationErrorс типомvalue_error. model_validator(mode="after")вызывается после проверки всех полей и получает готовый экземпляр — возвращайтеself. Режимmode="before"получает сырые входные данные (обычно словарь) до валидации.- По умолчанию
field_validatorработает в режимеafter— уже после проверки типа. Параметрmode="before"позволяет обработать сырое значение, например обрезать пробелы до проверки длины.
Ошибка из валидатора выглядит так:
1 validation error for SignUp
username
Value error, только буквы и цифры [type=value_error, input_value='anna_95', input_type=str]
Сериализация: model_dump и model_validate
Четыре метода покрывают почти все задачи преобразования:
| Метод | Что делает |
|---|---|
Model.model_validate(obj) | словарь (или объект) → модель с валидацией |
Model.model_validate_json(s) | JSON-строка → модель |
model.model_dump() | модель → словарь |
model.model_dump_json() | модель → JSON-строка |
from datetime import datetime
from pydantic import BaseModel, Field
class Address(BaseModel):
city: str
street: str
class Customer(BaseModel):
id: int
name: str
address: Address
created_at: datetime
password: str = Field(exclude=True)
data = {
"id": 1,
"name": "Анна",
"address": {"city": "Москва", "street": "Тверская, 1"},
"created_at": "2026-10-09T12:00:00",
"password": "secret",
}
customer = Customer.model_validate(data)
print(customer.address.city)
# → Москва
print(customer.model_dump())
# → {'id': 1, 'name': 'Анна', 'address': {'city': 'Москва', 'street': 'Тверская, 1'}, 'created_at': datetime.datetime(2026, 10, 9, 12, 0)}
print(customer.model_dump(include={"id", "name"}))
# → {'id': 1, 'name': 'Анна'}
print(customer.model_dump_json())
# → {"id":1,"name":"Анна","address":{"city":"Москва","street":"Тверская, 1"},"created_at":"2026-10-09T12:00:00"}
Что здесь важно:
- вложенный словарь
addressавтоматически стал модельюAddress; - поле с
Field(exclude=True)не попадает в вывод — удобно для паролей; model_dump()оставляет Python-типы (datetime), аmodel_dump(mode="json")иmodel_dump_json()превращают их в JSON-совместимые строки;includeиexcludeвыбирают поля для конкретного вызова.
Копия модели с изменениями — customer.model_copy(update={"name": "Анна П."}): исходный объект не меняется. JSON Schema модели возвращает Customer.model_json_schema() — её FastAPI использует для документации.
Настройки модели: ConfigDict
Поведение модели настраивается атрибутом model_config:
from pydantic import BaseModel, ConfigDict, ValidationError
class Strict(BaseModel):
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
name: str
print(Strict(name=" Анна "))
# → name='Анна'
try:
Strict(name="Анна", age=30)
except ValidationError as e:
print(e.errors()[0]["loc"], e.errors()[0]["msg"])
# → ('age',) Extra inputs are not permitted
Полезные параметры:
extra="forbid"— запретить лишние поля (по умолчанию они молча игнорируются),extra="allow"— сохранить их;str_strip_whitespace=True— обрезать пробелы у строк;strict=True— строгий режим без приведения типов;frozen=True— неизменяемая модель;from_attributes=True— создавать модель из объектов с атрибутами, например из моделей ORM:BookOut.model_validate(orm_object).
pydantic-settings: настройки из переменных окружения
BaseSettings из пакета pydantic-settings читает конфигурацию из переменных окружения и .env-файла и валидирует её так же, как обычная модель. В Pydantic v2 этот класс вынесен в отдельный пакет:
pip install pydantic-settings
fastapi[standard] устанавливает его автоматически.
Файл .env:
DATABASE_URL=postgresql://user:pass@localhost:5432/shop
SECRET_KEY=dev-secret
DEBUG=true
ALLOWED_HOSTS=["localhost", "127.0.0.1"]
Файл config.py:
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
database_url: str
secret_key: SecretStr
debug: bool = False
allowed_hosts: list[str] = ["localhost"]
page_size: int = 20
settings = Settings()
from config import settings
print(settings.debug, settings.page_size, settings.allowed_hosts)
# → True 20 ['localhost', '127.0.0.1']
print(settings.secret_key)
# → **********
print(settings.secret_key.get_secret_value())
# → dev-secret
Как это работает:
- имя переменной окружения совпадает с именем поля, регистр по умолчанию не важен:
DATABASE_URL→database_url; - строки приводятся к типам поля:
"true"→True, JSON-список →list[str]; - переменные окружения важнее
.env: запуск сPAGE_SIZE=50дастsettings.page_size == 50, даже если в.envдругое значение; - обязательное поле без значения (нет ни переменной, ни строки в
.env) вызоветValidationErrorпри старте — приложение не запустится с неполной конфигурацией; SecretStrскрывает значение при выводе и в логах; настоящее значение — черезget_secret_value();env_prefix="APP_"вSettingsConfigDictзаставит читатьAPP_DEBUGвместоDEBUG.
Не добавляйте .env с реальными секретами в git — храните в репозитории только пример вроде .env.example.
Pydantic в FastAPI
FastAPI использует Pydantic-модели для тела запроса, ответа и настроек:
from functools import lru_cache
from typing import Annotated
from fastapi import Depends, FastAPI
from pydantic import BaseModel, Field
from config import Settings
@lru_cache
def get_settings() -> Settings:
return Settings()
app = FastAPI()
class OrderIn(BaseModel):
product_id: int
quantity: int = Field(ge=1, le=100)
comment: str | None = Field(default=None, max_length=500)
class OrderOut(BaseModel):
id: int
product_id: int
quantity: int
@app.post("/orders/", response_model=OrderOut)
def create_order(order: OrderIn):
return {"id": 1, **order.model_dump()}
@app.get("/info")
def info(settings: Annotated[Settings, Depends(get_settings)]):
return {"debug": settings.debug, "page_size": settings.page_size}
OrderIn— модель тела запроса: приquantity: 0FastAPI вернёт 422 с"loc": ["body", "quantity"].OrderOut— модель ответа: полеcommentв ответ не попадёт.get_settingsс@lru_cacheсоздаёт настройки один раз, а черезDependsих удобно подменять в тестах.
Как устроены эндпоинты, параметры и зависимости — в руководстве по FastAPI.
Отличия Pydantic v1 и v2
Pydantic v2 переписан: ядро валидации (pydantic-core) реализовано на Rust, а API моделей обновлён. Код для v1 частично работает в v2 с предупреждениями PydanticDeprecatedSince20, но часть изменений ломает совместимость.
| Pydantic v1 | Pydantic v2 |
|---|---|
model.dict() | model.model_dump() |
model.json() | model.model_dump_json() |
Model.parse_obj(data) | Model.model_validate(data) |
Model.parse_raw(json_str) | Model.model_validate_json(json_str) |
Model.from_orm(obj) | Model.model_validate(obj) + from_attributes=True |
model.copy() | model.model_copy() |
Model.schema() | Model.model_json_schema() |
@validator | @field_validator |
@root_validator | @model_validator |
class Config: | model_config = ConfigDict(...) |
orm_mode = True | from_attributes=True |
Field(regex=...) | Field(pattern=...) |
from pydantic import BaseSettings | from pydantic_settings import BaseSettings |
Изменения поведения, на которых чаще всего спотыкаются:
Optional[int]без значения по умолчанию теперь обязательное поле. В v1 оно неявно получалоNone. В v2 пишите явно:x: int | None = None.- Меньше неявных приведений. Число в поле
strбольше не превращается в строку, а1.5в полеint— ошибка, а не1. Field(regex=...)иfrom pydantic import BaseSettingsвызывают ошибку при определении модели или импорте, а не предупреждение.- Старые методы (
dict(),json(),parse_obj()) пока работают, но помечены устаревшими и будут удалены в v3.
Для постепенной миграции в v2 доступно пространство имён pydantic.v1 со старым API: from pydantic.v1 import BaseModel. Подробный список изменений — в официальном руководстве по миграции.
Частые ошибки
PydanticImportError: BaseSettings has been moved
Вы используете пример для v1. Установите pip install pydantic-settings и замените импорт на from pydantic_settings import BaseSettings.
Поле Optional стало обязательным
В v2 Optional[str] означает «может быть None», но не «необязательное». Добавьте значение по умолчанию: name: str | None = None.
Валидатор ничего не возвращает
После проверки в field_validator поле стало None? Метод должен вернуть значение (return value). То же для model_validator(mode="after") — он возвращает self.
Лишние поля молча пропадают
По умолчанию Pydantic игнорирует ключи, которых нет в модели. Если опечатка в имени поля должна давать ошибку, включите model_config = ConfigDict(extra="forbid").
ModuleNotFoundError: No module named ‘pydantic_settings’
Пакет не установлен в текущее окружение: pip install pydantic-settings. Обратите внимание: в pip install имя пишется через дефис, в import — через подчёркивание. Другие причины — в статье про ModuleNotFoundError.
Частые вопросы
Чем Pydantic отличается от dataclasses?
Стандартный @dataclass только создаёт класс с полями и не проверяет типы: для поля id: int значение "42" так и останется строкой. Pydantic валидирует и приводит данные, выдаёт ValidationError, умеет сериализацию в JSON и JSON Schema. Если нужен синтаксис dataclass с проверкой, есть from pydantic.dataclasses import dataclass — там id="42" превратится в 42. Для внутренних структур без внешнего ввода обычного dataclass достаточно.
Как принимать JSON с ключами в camelCase?
Задайте генератор псевдонимов: поля в Python останутся в snake_case, а во входных и выходных данных будут в camelCase.
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class Order(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, validate_by_name=True)
order_id: int
customer_name: str
order = Order.model_validate({"orderId": 7, "customerName": "Анна"})
print(order.model_dump(by_alias=True))
# → {'orderId': 7, 'customerName': 'Анна'}
validate_by_name=True (Pydantic 2.11+) разрешает создавать модель и по именам полей: Order(order_id=8, customer_name="Олег"). В более старых версиях для этого служит populate_by_name=True.
Как проверить список объектов без модели-обёртки?
Используйте TypeAdapter — он валидирует любой тип, а не только BaseModel:
from pydantic import BaseModel, TypeAdapter
class User(BaseModel):
id: int
name: str
users = TypeAdapter(list[User]).validate_python([{"id": "1", "name": "Анна"}])
print(users)
# → [User(id=1, name='Анна')]
Для JSON-строки есть validate_json(). Создавайте адаптер один раз и переиспользуйте: его построение дороже самой проверки.
Как добавить в вывод вычисляемое поле?
Декоратор @computed_field над свойством включает его в model_dump(), JSON и схему:
from pydantic import BaseModel, computed_field
class Rectangle(BaseModel):
width: float
height: float
@computed_field
@property
def area(self) -> float:
return self.width * self.height
print(Rectangle(width=3, height=4).model_dump())
# → {'width': 3.0, 'height': 4.0, 'area': 12.0}
Во входных данных такое поле не принимается — оно всегда вычисляется из остальных.
Итог
Pydantic превращает аннотации типов в проверку данных: модель описывает структуру, Field и валидаторы — правила, model_validate и model_dump — преобразование в обе стороны, а ValidationError подробно объясняет, что не так. Для конфигурации используйте pydantic-settings, а при переходе с v1 сверяйтесь с таблицей выше.
Чтобы применить Pydantic на практике, переходите к руководству по FastAPI или сразу установите FastAPI. Все материалы раздела — на странице FastAPI для начинающих.
- #Pydantic
- #Python
- #FastAPI
- #Валидация
- #pydantic-settings