Pydantic для валидации данных на собеседовании Data Engineer
campaign_a(user_id) и campaign_b(user_id). Как корректнее объединить списки, чтобы убрать дубликаты?Содержание:
Зачем Pydantic в ETL
ETL-пайплайны принимают сырые данные из API, файлов и очередей — то есть из источников, которым нельзя доверять. Стоит поставщику поменять тип поля, прислать NULL вместо числа или переименовать ключ в JSON, и без проверки этот мусор молча поедет дальше по пайплайну, а всплывёт уже в дашборде или в упавшей ML-модели. Отлаживать такое потом — боль: ошибка проявляется далеко от места, где данные испортились.
Pydantic решает эту проблему на входе. Это библиотека для валидации данных на основе аннотаций типов Python: вы описываете ожидаемую схему как обычный класс, а Pydantic проверяет входные данные, приводит типы там, где это разумно, и падает с понятной ошибкой, если данные не соответствуют контракту. На собесе Data Engineer это любимый инструмент для темы «валидация и контракты данных», потому что он ловит проблемы рано — на границе пайплайна, а не в его конце.
Модели
Модель — это класс, унаследованный от BaseModel, где поля описаны аннотациями типов. Валидация происходит автоматически при создании объекта:
from datetime import datetime
from pydantic import BaseModel
class Order(BaseModel):
id: int
amount: float
status: str
created_at: datetime
# Валидация запускается автоматически
order = Order(**raw_dict) # бросит ValidationError, если данные не подходятЧто Pydantic делает при создании объекта:
- Проверяет типы. Поле
amountдолжно быть числом,created_at— датой. - Требует обязательные поля. Если в
raw_dictнетid, будет ошибка — поле без значения по умолчанию считается обязательным. - Приводит типы, где это разумно. Строку
"42"в полеintпревратит в число42.
Если данные не проходят проверку, поднимается ValidationError со списком всех проблем сразу — это удобнее, чем ловить ошибки по одной.
Приведение типов
По умолчанию Pydantic работает в «мягком» режиме и приводит совместимые типы автоматически. Это ровно то, что нужно в ETL, где API часто отдаёт числа и даты строками:
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel
class Event(BaseModel):
timestamp: datetime
user_id: int
amount: Decimal
raw = {"timestamp": "2026-05-07", "user_id": "42", "amount": "100.50"}
event = Event(**raw) # всё корректно приведётся к нужным типамЗдесь строка "2026-05-07" станет datetime, "42" — числом, "100.50" — Decimal. Но иногда приведение вредит: если поле критично и вы хотите отклонять строку там, где ждёте только число, включается строгий режим (strict=True в конфигурации модели или поля). В нём приведение отключается, и "42" в поле int уже вызовет ошибку. На собесе стоит проговорить этот компромисс: мягкий режим удобнее для грязных источников, строгий — надёжнее для критичных контрактов.
Валидаторы
Когда встроенных проверок типов мало и нужна своя бизнес-логика, пишут кастомные валидаторы. Метод помечается декоратором field_validator, принимает значение поля и либо возвращает его (возможно, изменённым), либо бросает ошибку:
from pydantic import BaseModel, field_validator
class User(BaseModel):
email: str
age: int
@field_validator('age')
def age_positive(cls, v):
if v < 0:
raise ValueError('возраст не может быть отрицательным')
return v
@field_validator('email')
def email_valid(cls, v):
if '@' not in v:
raise ValueError('некорректный email')
return v.lower()Валидатор может не только проверять, но и нормализовать данные — здесь email заодно приводится к нижнему регистру. Это удобное место для правил, которые не выразить одним типом: «дата отгрузки не раньше даты заказа», «сумма положительная», «код страны из списка ISO». Для проверок, которые затрагивают несколько полей сразу, есть model_validator.
Pydantic v1 vs v2
Pydantic v2 (вышел в 2023) — это крупная переработка библиотеки, и на собесе про отличия спрашивают часто, потому что в продакшене встречаются оба поколения:
- Скорость. Ядро v2 переписано на Rust (
pydantic-core), что дало ускорение в 5–50 раз на реальных нагрузках. - Сообщения об ошибках. Стали подробнее и структурированнее — проще понять, какое поле и почему не прошло.
- API поменялся. Часть изменений ломающая: переименованы декораторы и методы конфигурации.
# v1
class Model(BaseModel):
@validator('field')
...
# v2
class Model(BaseModel):
@field_validator('field')
...В новых проектах берут v2 — он быстрее и активно развивается. Legacy-кодовые базы мигрируют постепенно, для этого есть инструмент bump-pydantic, который автоматически переписывает часть устаревшего синтаксиса.
Как это спрашивают на собесе
Реальные формулировки и что за ними проверяют:
- «Зачем валидировать данные на входе ETL, а не в конце?» Чтобы поймать битые данные до того, как они разъедутся по пайплайну и всплывут в дашборде. Отладка у источника кратно дешевле.
- «Чем Pydantic лучше ручных проверок через if?» Схема описывается декларативно через типы, ошибки собираются все сразу в
ValidationError, а не по одной, плюс бесплатное приведение типов. - «Как отклонить строку там, где ждёте int?» Включить strict-режим — в нём приведение отключается и несовпадение типа даёт ошибку.
- «Как проверить правило, зависящее от двух полей?» Через
model_validator, который валидирует всю модель целиком, а не отдельное поле. - «Чем v2 отличается от v1?» Rust-ядро и ускорение в разы, новые декораторы (
field_validatorвместоvalidator), более детальные ошибки.
Частые ошибки
- Полагаться на мягкое приведение для критичных полей. Pydantic по умолчанию с радостью превратит
"0"в0или"true"вTrue. Для полей, где важна строгость, включайте strict-режим. - Валидировать в конце пайплайна. Тогда битые данные уже успели повлиять на промежуточные шаги. Проверять надо на границе — сразу после чтения из источника.
- Игнорировать содержимое ValidationError. Ошибка содержит список всех проблем с указанием полей — её надо логировать и мониторить, а не просто ловить и глотать.
- Тяжёлые валидаторы на горячем пути. Валидатор с обращением к БД или внешнему API на каждую строку убьёт производительность батча. Тяжёлые проверки выносят отдельно.
- Путать синтаксис v1 и v2.
@validatorиз v1 и@field_validatorиз v2 несовместимы; смешивание в одном проекте — источник неочевидных багов.
Связанные темы
- Schema evolution для DE
- Data contracts для DE
- Great Expectations для DE
- ETL pitfalls для DE
- Подготовка к собесу Data Engineer
FAQ
Чем Pydantic лучше ручной проверки данных?
Схема описывается декларативно через аннотации типов, а не десятком проверок if. Pydantic сам приводит совместимые типы, требует обязательные поля и собирает все ошибки в один ValidationError вместо того, чтобы падать на первой. Код становится короче, а проверки — единообразными и переиспользуемыми.
Когда включать strict-режим?
Когда автоматическое приведение типов вредит: для критичных полей, где строка вместо числа или "true" вместо булева — это сигнал о битом источнике, а не то, что нужно молча исправить. В мягком режиме такие значения приведутся, в strict — дадут ошибку. Для грязных внешних API обычно оставляют мягкий режим, для внутренних контрактов — строгий.
Как проверить правило, которое зависит от нескольких полей?
Через model_validator — он валидирует модель целиком, когда все поля уже разобраны. Подходит для правил вроде «дата окончания позже даты начала» или «если тип = premium, поле план обязательно». field_validator для этого не годится, он видит только одно поле.
Стоит ли использовать v1 или v2?
В новых проектах — v2: ядро на Rust даёт ускорение в 5–50 раз, ошибки подробнее, библиотека активно развивается. v1 остаётся в legacy-проектах; миграцию упрощает инструмент bump-pydantic. Синтаксис у поколений разный (validator против field_validator), смешивать их не стоит.
Это официальная информация?
Нет. Статья основана на документации Pydantic (v2) и опыте кандидатов. Конкретный стек и требования зависят от команды и проекта.
Тренируйте Data Engineering — откройте тренажёр с 1500+ вопросами для собесов.