Django-Admin.ru

Pydantic: валидация данных в Python

Что такое Pydantic и как работать с v2: BaseModel, Field, field_validator, model_dump и model_validate, ValidationError, pydantic-settings и переход с v1.

Редакция Django-Admin.ru
· обновлено · 13 мин чтения

Содержание · 13 разделов
  1. Установка
  2. BaseModel: модели и типы
  3. Ограничения полей: Field
  4. ValidationError: как читать ошибки
  5. Свои валидаторы: field_validator и model_validator
  6. Сериализация: model_dump и model_validate
  7. Настройки модели: ConfigDict
  8. pydantic-settings: настройки из переменных окружения
  9. Pydantic в FastAPI
  10. Отличия Pydantic v1 и v2
  11. Частые ошибки
  12. PydanticImportError: BaseSettings has been moved
  13. Поле Optional стало обязательным
  14. Валидатор ничего не возвращает
  15. Лишние поля молча пропадают
  16. ModuleNotFoundError: No module named ‘pydantic_settings’
  17. Частые вопросы
  18. Чем Pydantic отличается от dataclasses?
  19. Как принимать JSON с ключами в camelCase?
  20. Как проверить список объектов без модели-обёртки?
  21. Как добавить в вывод вычисляемое поле?
  22. Итог

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
int1.01
int1.5ошибка: дробная часть
str123ошибка: ожидается строка
bool"yes", "on", 1True
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: 0 FastAPI вернёт 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 v1Pydantic 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 = Truefrom_attributes=True
Field(regex=...)Field(pattern=...)
from pydantic import BaseSettingsfrom 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
↑ ↓ выборEnter открытьEsc закрыть