← Назад к курсу

Валидация данных в Python при помощи Pydantic

Введение в Pydantic

Pydantic - это библиотека для валидации данных и управления настройками с использованием аннотаций типов Python. Она позволяет определять структуры данных с проверкой типов во время выполнения.

Установка

pip install pydantic
# Для последней версии (Pydantic V2)
pip install pydantic[email]

Основы Pydantic

1. Создание базовой модели

from pydantic import BaseModel
from typing import Optional, List

# Определяем модель
class User(BaseModel):
    id: int
    username: str
    email: str
    age: Optional[int] = None  # Необязательное поле со значением по умолчанию
    is_active: bool = True  # Поле со значением по умолчанию

# Создаем экземпляр модели
user1 = User(
    id=1,
    username="john_doe",
    email="john@example.com",
    age=25
)

print(user1)
print(user1.username)
print(user1.dict())  # Преобразование в словарь
print(user1.json())  # Преобразование в JSON

2. Валидация данных

Pydantic автоматически проверяет типы данных:

from pydantic import ValidationError

try:
    user2 = User(
        id="not_a_number",  # Будет ошибка!
        username="test",
        email="test@example.com"
    )
except ValidationError as e:
    print("Ошибка валидации:")
    print(e.json())

3. Модели с вложенными структурами

class Address(BaseModel):
    street: str
    city: str
    country: str

class FullUser(BaseModel):
    id: int
    username: str
    address: Address  # Вложенная модель
    friends: List[int] = []  # Список чисел

# Создаем вложенную структуру
address = Address(street="Main St", city="New York", country="USA")
user3 = FullUser(
    id=1,
    username="alice",
    address=address,
    friends=[2, 3, 4]
)

print(user3.dict())

Валидаторы (Validators)

Pydantic позволяет создавать собственные валидаторы:

from pydantic import validator, Field
import re

class Product(BaseModel):
    name: str = Field(..., min_length=3, max_length=50)
    price: float = Field(..., gt=0)  # Больше 0
    sku: str  # Артикул товара
    
    @validator('sku')
    def validate_sku(cls, value):
        # Проверяем формат артикула (3 буквы, 6 цифр)
        if not re.match(r'^[A-Z]{3}\d{6}$', value):
            raise ValueError('SKU должен быть в формате ABC123456')
        return value
    
    @validator('name')
    def name_must_contain_space(cls, value):
        if ' ' not in value:
            raise ValueError('Название должно содержать пробел')
        return value.title()

# Использование
try:
    product = Product(name="test product", price=19.99, sku="ABC123456")
    print(product)
except ValidationError as e:
    print(e)

Конфигурация моделей

from pydantic import ConfigDict

class ConfigExample(BaseModel):
    # Конфигурация модели
    model_config = ConfigDict(
        frozen=True,  # Объекты нельзя изменять после создания
        str_strip_whitespace=True,  # Автоматически удаляет пробелы в строках
        extra='forbid'  # Запрещает дополнительные поля
    )
    
    name: str
    value: int

obj = ConfigExample(name="  test  ", value=10)
print(obj.name)  # "test" (без пробелов)

# obj.value = 20  # Ошибка! Объект frozen

Работа с датами и временем

from datetime import datetime, date
from pydantic import field_validator  # В Pydantic V2

class Event(BaseModel):
    title: str
    start_date: date
    end_date: date
    created_at: datetime = Field(default_factory=datetime.now)
    
    @field_validator('end_date')
    def validate_dates(cls, end_date, info):
        start_date = info.data.get('start_date')
        if start_date and end_date < start_date:
            raise ValueError('Дата окончания не может быть раньше даты начала')
        return end_date

event = Event(
    title="Конференция",
    start_date=date(2024, 6, 1),
    end_date=date(2024, 6, 3)
)

print(event)

Парсинг из различных источников

Pydantic умеет парсить данные из разных форматов:

class Item(BaseModel):
    name: str
    price: float
    quantity: int = 1

# Из словаря
data_dict = {"name": "Book", "price": 29.99}
item1 = Item(**data_dict)

# Из JSON
import json
json_data = '{"name": "Laptop", "price": 999.99, "quantity": 2}'
item2 = Item.parse_raw(json_data)

# Из файла
with open('item.json', 'w') as f:
    f.write(json_data)

item3 = Item.parse_file('item.json')

Generic модели (обобщенные модели)

from typing import Generic, TypeVar, List
from pydantic import BaseModel

T = TypeVar('T')

class Response(BaseModel, Generic[T]):
    success: bool
    data: T
    message: str = ""

# Использование с разными типами
user_response = Response[User](
    success=True,
    data=user1,
    message="User retrieved successfully"
)

list_response = Response[List[User]](
    success=True,
    data=[user1],
    message="Users retrieved successfully"
)

Работа с API (FastAPI пример)

Pydantic отлично интегрируется с FastAPI:

from fastapi import FastAPI, HTTPException
from typing import List

app = FastAPI()

# Модели для API
class ItemCreate(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

class ItemResponse(ItemCreate):
    id: int

# "База данных"
items_db = []

@app.post("/items/", response_model=ItemResponse)
async def create_item(item: ItemCreate):
    # Pydantic автоматически валидирует входящие данные
    item_dict = item.dict()
    item_dict['id'] = len(items_db) + 1
    items_db.append(item_dict)
    return item_dict

@app.get("/items/", response_model=List[ItemResponse])
async def read_items():
    return items_db

Полезные декораторы и методы

class SmartModel(BaseModel):
    name: str
    value: int
    
    # Метод вызывается после валидации
    def model_post_init(self, __context):
        print(f"Создан объект {self.name}")
    
    # Кастомный метод
    def double_value(self):
        return self.value * 2

# Копирование с обновлением
obj1 = SmartModel(name="Test", value=10)
obj2 = obj1.model_copy(update={'value': 20})
print(obj2.double_value())  # 40

Практический пример: система заказов

from enum import Enum
from typing import Dict

class OrderStatus(str, Enum):
    PENDING = "pending"
    PROCESSING = "processing"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

class OrderItem(BaseModel):
    product_id: int
    product_name: str
    quantity: int
    price_per_unit: float
    
    @property
    def total_price(self):
        return self.quantity * self.price_per_unit

class Order(BaseModel):
    order_id: int
    customer_id: int
    items: List[OrderItem]
    status: OrderStatus = OrderStatus.PENDING
    metadata: Dict[str, str] = {}
    
    @property
    def order_total(self):
        return sum(item.total_price for item in self.items)
    
    def add_item(self, product_id: int, product_name: str, 
                 quantity: int, price_per_unit: float):
        self.items.append(
            OrderItem(
                product_id=product_id,
                product_name=product_name,
                quantity=quantity,
                price_per_unit=price_per_unit
            )
        )
    
    def update_status(self, new_status: OrderStatus):
        valid_transitions = {
            OrderStatus.PENDING: [OrderStatus.PROCESSING, OrderStatus.CANCELLED],
            OrderStatus.PROCESSING: [OrderStatus.SHIPPED, OrderStatus.CANCELLED],
            OrderStatus.SHIPPED: [OrderStatus.DELIVERED],
        }
        
        if new_status in valid_transitions.get(self.status, []):
            self.status = new_status
        else:
            raise ValueError(f"Нельзя перейти из {self.status} в {new_status}")

# Использование
order = Order(
    order_id=1,
    customer_id=123,
    items=[
        OrderItem(
            product_id=1,
            product_name="Python Book",
            quantity=2,
            price_per_unit=29.99
        )
    ]
)

order.add_item(2, "Pydantic Guide", 1, 15.99)
print(f"Total: ${order.order_total:.2f}")
order.update_status(OrderStatus.PROCESSING)

Советы и лучшие практики

  1. Всегда используйте аннотации типов
  2. Используйте Field для дополнительных ограничений
  3. Разделяйте модели для ввода и вывода (например, UserCreate и UserResponse)
  4. Используйте ConfigDict для настройки поведения моделей
  5. Проверяйте данные как можно раньше

Заключение

Pydantic - мощный инструмент для валидации данных в Python. Он помогает:

  • Обеспечить целостность данных
  • Уменьшить количество ошибок
  • Улучшить читаемость кода
  • Легко интегрироваться с другими инструментами (FastAPI, SQLAlchemy и др.)

Начните использовать Pydantic в своих проектах для более надежной работы с данными!