Python pydantic. Валидация данных
В мире веб-разработки обработка и валидация данных играют ключевую роль. Изучая FastAPI я познакомился с замечательной библиотекой Pydantic. В статье "Введение FastAPI" я описал использование Pydantic в связке с SQLAlchemy. А в данной статье мы рассмотрим все аспекты использования Pydantic, начиная от базовых концепций до более продвинутых техник валидации данных в Python.
Примечание: В данной статье рассматривается Pydantic 1
Что такое Pydantic?
Pydantic - это библиотека , для валидации и сериализации данных в Python. Pydantic предоставляет усовершенствованные возможности для валидации данных и управления их структурой с использованием аннотаций типов в Python. Эта библиотека обеспечивает подсказки типов на этапе выполнения и генерирует информативные сообщения об ошибках, когда данные не соответствуют заданным условиям.
Основные возможности Pydantic
Аннотации типов: Определение моделей данных с использованием аннотаций типов для атрибутов, что делает код более ясным и читаемым.
Валидация данных:
Сериализация данных: Легкость преобразования данных в формат JSON и обратно, что делает взаимодействие с API и хранение данных более удобным.
Поддержка дефолтных значений: Задание значений по умолчанию для атрибутов, что упрощает работу с необязательными данными.
Установим pydantic
pip install pydantic
Модели
В Pydantic основным инструментом для определения объектов данных являются модели. Модели в Pydantic представляют собой классы, которые наследуются от BaseModel. Эти модели описывают структуру данных, задавая типы и ограничения для их атрибутов.
Создадим первую Pydantic модель для представления поста(Post). Для этого нам нужно просто создать класс, который наследуется от BaseModel. Затем мы можем передать сырые данные в эту модель и посмотреть, как Pydantic c может преобразовать их в структурированный объект данных.
from datetime import datetime
from pydantic import BaseModel
#Наследуемся от BaseModel
class Post(BaseModel):
id: int
title: str
text: str
is_published: bool
tags: list[str] = []
published_at: datetime | None = None
if __name__ == '__main__':
input_data = {
'id': '1',
'title': 'Python decorators',
'text': 'Python decorators text',
'is_published': True,
'published_at': '2019-06-01 12:22',
'tags': ['python', 'js', 3],
'another_field': 'Test data',
}
post = Post(**input_data)
print(post.id, type(post.id))
#> 1
print(post.published_at, repr(post.published_at))
#> 2019-06-01 12:22:00 datetime.datetime(2019, 6, 1, 12, 22)
print("Выводим словарь", post.dict())
#> Выводим словарь {'id': 1, 'title': 'Pyton decorators', 'text': 'Python decorators text',
# 'is_published': True, 'tags': ['python', 'js', '3'], 'published_at': datetime.datetime(2019, 6, 1, 12, 22)}
print("Выводим данные в json", post.json())
#> Выводим данные в json {"id": 1, "title": "Python decorators", "text": "Python decorators text",
# "is_published": true, "tags": ["python", "js", "3"], "published_at": "2019-06-01T12:22:00"}
Разберем поподробнее поля класса Post:
-
id - имеет целочисленный тип(int) и это поле является обязательным. Для этого поля мы передали строковое значение , но pydantic преобразовал его в целочисленный тип.
Строки(str), байты(bytes) и числа плавающей запятой pydantic по возможности преобразует в целочисленный. В противном случае возбуждается исключение.
from pydantic import BaseModel class Example(BaseModel): id: int print(Example(id='1')) # строковое значние будет преобразовано в 1 print(Example(id=1.123)) # float преобразуется в 1 (отсекается дробовая часть) # Возникнет исключение валидации из-за невозможности преобразовать в целочисленный print(Example(id='this is str')) #> pydantic.error_wrappers.ValidationError: 1 validation error for Example # id value is not a valid integer (type=type_error.integer)
name - поле имеет строковый тип и является обязательным.
text - поле имеет строковый тип и является обязательным.
is_published - поле имеет булев тип и по умолчанию имеет значение True(Истинно).
is_published - поле имеет булев тип и по умолчанию имеет значение True(Истинно).
is_published - поле имеет булев тип и по умолчанию имеет значение
published_at - это поле даты и времени, которое не требуется (и принимает значение None, если оно не указано). Pydantic будет обрабатывать либо временную метку unix int (например, 1496498400), либо строку, представляющую дату и время.
tags- требует список строковых значений. По возможности входящие значения Pydanticбудет преобразоывать в строковые значения.
Выше мы создаем объект модели Post и передаем данные. Так как данные валидные , то создается новый объект и при этом как указали выше , некоторые входные данные преобразуются в тот тип , который указан в аннотациях полей.Во входных данных(input_data) у нас есть поле another_field, но такого поля нет в модели и поэтому значение этого поля игнорируется
Если входные данные будут неправильными с точки зрения описания модели , то будет вызываться ошибка валидации. Эти исключения мы можем обрабатывать следующим образом:
from pydantic import ValidationError
try:
post = Post(**input_data)
except ValidationError as e:
print(e.json())
Далее мы используем два метода , которые выводят данные в виде словаря(метод BaseModel.dict()) и виде json (метод BaseModel.json()). Есть еще другие методы и атрибуты модели , которые мы рассмотрим ниже.
Методы модели BaseModel
dict() - Возвращает словарь поля и значения модели
json() - Возвращает строковое представление JSON метода dict()
copy() - Возвращает копию модели(по умолчанию поверхностное копирование)
parse_obj()- метод для загрузки любого объекта в модель с обработкой ошибок, если объект не является словарем
parse_raw() - метод для загрузки строк различных форматов
parse_file() - похож на parse_raw() но для путей к файлам;
from_orm() - загружает данные в модель из произвольного класса; (режим ORM)
-
schema() - возвращает словарь, представляющий модель в виде схемы JSON.
schema_json() - Возвращает строковое представление JSON метода schema()
from datetime import datetime from pydantic import BaseModel class Post(BaseModel): id: int title: str text: str is_published: bool tags: list[str] = [] published_at: datetime | None = None input_data = { 'id': '1', 'title': 'Python decorators', 'text': 'Python decorators text', 'is_published': True, 'published_at': '2019-06-01 12:22', 'tags': ['python', 'js', 3], 'another_field': 'Test data', } post = Post(**input_data) print(post.schema()) """ {'title': 'Post', 'type': 'object', 'properties': {'id': {'title': 'Id', 'type': 'integer'}, 'title': {'title': 'Title', 'type': 'string'}, 'text': {'title': 'Text', 'type': 'string'}, 'is_published': {'title': 'Is Published', 'type': 'boolean'}, 'tags': {'title': 'Tags', 'default': [], 'type': 'array', 'items': {'type': 'string'}}, 'published_at': {'title': 'Published At', 'type': 'string', 'format': 'date-time'}}, 'required': ['id', 'title', 'text', 'is_published']} """ print(post.schema_json()) """ {"title": "Post", "type": "object", "properties": {"id": {"title": "Id", "type": "integer"}, "title": {"title": "Title", "type": "string"}, "text": {"title": "Text", "type": "string"}, "is_published": {"title": "Is Published", "type": "boolean"}, "tags": {"title": "Tags", "default": [], "type": "array", "items": {"type": "string"}}, "published_at": {"title": "Published At", "type": "string", "format": "date-time"}}, "required": ["id", "title", "text", "is_published"]} """
construct() - метод класса для создания моделей без запуска проверки
Это может быть полезно, когда данные уже проверены или получены из надежного источника, и вы хотите создать модель максимально эффективно (construct() обычно в 30 раз быстрее, чем создание модели с полной проверкой).
Рекурсивные(вложенные) модели
Более сложные иерархические структуры данных можно определить, используя сами модели в качестве типов в аннотациях.
Давайте создадим модель Author со своими полями и эту модель мы будем использовать для указания аннотации для поля authors в модели Post
from datetime import datetime
from pydantic import BaseModel, EmailStr
class Author(BaseModel):
name: str
last_name: str
username: str
email: EmailStr #EmailStr - это тип данных из Pydantic, который валидирует, что строка имеет правильный формат электронной почты.
date_of_birth: str
avatar: str
class Post(BaseModel):
id: int
title: str
text: str
is_published: bool
tags: list[str] = []
published_at: datetime | None = None
authors: list[Author] #Список авторов поста. Это список объектов Author, связанных с данным постом.
# Создание экземпляра Author
author = Author(
name="John",
last_name="Doe",
username="johndoe",
email="johndoe@example.com",
date_of_birth="1990-01-01",
avatar="https://example.com/avatar.jpg"
)
# Создание экземпляра Post с автором
post = Post(
id=1,
title="Sample Post",
text="This is the content of the post",
is_published=True,
tags=["python", "programming"],
published_at=datetime.now(),
authors=[author]
)
Валидаторы в Pydantic
В Pydantic, помимо базовых аннотаций типов, вы также можете использовать валидаторы для более тонкой настройки валидации данных в ваших моделях. Валидаторы - это функции, которые выполняют проверку и трансформацию данных, прежде чем они будут присвоены атрибутам модели
Чтобы добавить валидатор к атрибуту модели, вы можете использовать декоратор @validator. Давайте рассмотрим примеры использования валидаторов:
Допустим, мы хотим добавить валидатор для поля date_of_birth, чтобы убедиться, что дата рождения автора находится в прошлом.
from pydantic import BaseModel, EmailStr, validator
from datetime import date
class Author(BaseModel):
name: str
last_name: str
username: str
email: EmailStr
date_of_birth: str
avatar: str
@validator("date_of_birth")
def check_date_of_birth(cls, value):
# Преобразуем строку с датой в объект date
date_of_birth = date.fromisoformat(value)
today = date.today()
if date_of_birth >= today:
raise ValueError("Дата рождения должна быть в прошлом")
return value
Создадим комбинированный валидатор для модели Post, который будет проверять несколько условий одновременно.
Например, мы можем создать валидатор, который проверяет, что текст поста не пустой, а если пост опубликован (is_published установлен в True), то также проверяет наличие тегов (tags)
from pydantic import BaseModel, validator
class Post(BaseModel):
id: int
title: str
text: str
is_published: bool
tags: list[str] = []
@validator("text")
def check_text_not_empty(cls, value, values):
if not value:
raise ValueError("Текст поста не может быть пустым")
return value
@validator("tags")
def check_tags_if_published(cls, value, values):
is_published = values.get("is_published")
if is_published and not value:
raise ValueError("Если пост опубликован, необходимо указать хотя бы один тег")
return value
В этом примере мы создали два валидатора:
-
check_text_not_empty: Этот валидатор проверяет, что текст поста не пустой. Он использует аргумент value для доступа к значению поля text.
-
check_tags_if_published: Этот валидатор проверяет наличие тегов только если пост опубликован (is_published равен True). Он использует аргументы value и values для доступа к значению поля tags и is_published.
Таким образом, комбинированный валидатор может учитывать несколько полей и их значения при валидации данных в модели. Это позволяет определять сложные правила валидации, которые зависят от разных атрибутов модели.
Заключение
В этой статье мы показали, как создавать модели , как объявлять поля с типами аннотаций, показали как pydantic пытается преобразовать входные данные
Особое внимание было уделено методам моделей, которые делают работу с данными более удобной. Рассмотрели вложенные модели, а также показали примеры использования валидаторов
Pydantic предоставляет множество мощных возможностей для работы с данными в Python, и планируется выход второй версии , где будет много изменений и классных улучшений.