Kurs Python · Moduł 6: Async i FastAPI

Pydantic - walidacja danych z Rust

5 min czytania
W tej lekcji5

Witaj! Darwin tutaj. Strażnik parku wpisał w formularzu populację "sto dwadzieścia" zamiast 120, a datę w polu gatunku. Bez kontroli taki wpis trafi do bazy i zepsuje statystyki całego rezerwatu. Pydantic to biblioteka do walidacji i serializacji danych oparta na type hints: opisujesz kształt danych adnotacjami typów, a ona sprawdza każdą wartość.

Pydantic to rdzeń FastAPI - framework waliduje nim parametry i ciała żądań oraz kształtuje odpowiedzi. W wersji 2 logikę walidacji przeniesiono do pakietu pydantic-core napisanego w Rust, a według autorów typowy model sprawdza się około 17× szybciej niż w wersji 1.

Analogia Safari: Pydantic to jak system kontroli jakości obserwacji - sprawdza, czy każda obserwacja ma poprawne dane (gatunek istnieje, populacja to liczba, data jest prawidłowa). Jeśli coś jest nie tak, od razu zgłasza błąd!

Podstawy Pydantic

BaseModel - klasa bazowa

Model to klasa dziedzicząca po BaseModel. Każde pole to nazwa z adnotacją typu, a wartość po znaku = staje się domyślną:

1from pydantic import BaseModel
2
3class Species(BaseModel):
4    id: int
5    name: str
6    population: int
7    endangered: bool = False  # Domyślna wartość
8
9# Tworzenie instancji
10lion = Species(id=1, name="Lew", population=120, endangered=True)
11
12print(lion.name)               # Lew
13print(lion.model_dump())       # {'id': 1, 'name': 'Lew', 'population': 120, 'endangered': True}
14print(lion.model_dump_json())  # {"id":1,"name":"Lew","population":120,"endangered":true}

model_dump() zamienia model na słownik, a model_dump_json() na tekst JSON. Starsze tutoriale używają .dict() i .json() - w Pydantic 2 wciąż działają, ale zgłaszają ostrzeżenie i znikną w wersji 3.

Pola opcjonalne

Nie każda obserwacja ma opis. Pole, które może zostać puste, oznaczamy typem Optional[str] i wartością domyślną None:

1from typing import Optional
2from pydantic import BaseModel
3
4class Sighting(BaseModel):
5    species: str
6    count: int
7    description: Optional[str] = None
8
9print(Sighting(species="Lew", count=3))  # species='Lew' count=3 description=None

Samo Optional[str] pozwala przekazać None, ale dopiero = None pozwala pole pominąć. Od Pythona 3.10 to samo zapiszesz jako str | None = None.

Automatyczna konwersja typów

Dane z formularzy i adresów URL przychodzą jako tekst. Pydantic próbuje je przekonwertować na zadeklarowany typ:

1# Pydantic automatycznie konwertuje typy!
2species = Species(id="1", name="Lew", population="120", endangered="yes")
3
4print(type(species.id))         # <class 'int'>  (konwersja str→int)
5print(type(species.population)) # <class 'int'>
6print(species.endangered)       # True  (konwersja "yes"→True)

Konwersja zachodzi tylko bez utraty informacji: "120" staje się liczbą, ale "abc" czy 1.5 do pola int nie przejdą. Pydantic nie wstawia wtedy None, tylko zgłasza błąd.

Walidacja błędów

Gdy konwersja się nie uda, Pydantic zgłasza ValidationError z listą wszystkich problemów. Łapiemy go jak każdy wyjątek:

1from pydantic import ValidationError
2
3try:
4    # Błędne dane
5    species = Species(id="abc", name="Lew", population=-50)
6except ValidationError as e:
7    print(e.error_count())  # 1
8    print(e.json())         # Szczegółowe błędy walidacji (JSON)

Błąd jest jeden, bo -50 to poprawny int - model nie zna reguły, że populacja nie może być ujemna. Takie reguły dodaje Field.

Field - zaawansowana walidacja

Field dopisuje do pola ograniczenia: zakresy liczb, długość tekstu, wzorzec. Trzy kropki ... oznaczają pole wymagane:

1from pydantic import BaseModel, Field
2
3class Species(BaseModel):
4    id: int = Field(..., gt=0, description="Unique species ID")
5    name: str = Field(..., min_length=2, max_length=100)
6    scientific_name: str = Field(..., pattern=r'^[A-Z][a-z]+ [a-z]+$')  # Regex
7    population: int = Field(..., ge=0, le=1_000_000)  # >= 0, <= 1M
8    habitat: str = Field(default="unknown")
9
10# Walidacja działa!
11lion = Species(
12    id=1,
13    name="Lew",
14    scientific_name="Panthera leo",  # Musi pasować do regex
15    population=120
16)

Teraz population=-50 zostałoby odrzucone przez ge=0. Zapis Field(gt=0) bez wartości domyślnej też oznacza pole wymagane - krótszy, więc go polecam.

Field validators:

  • gt, ge - greater than, greater or equal
  • lt, le - less than, less or equal
  • min_length, max_length - długość string
  • pattern - regex validation
  • description - opis dla dokumentacji

Custom validators

Nietypową regułę zapisujesz jako funkcję z dekoratorem @field_validator. Zwraca ona wartość albo zgłasza ValueError:

1from pydantic import BaseModel, ValidationError, field_validator
2
3class Species(BaseModel):
4    name: str
5    population: int
6
7    @field_validator('name')
8    @classmethod
9    def name_must_be_capitalized(cls, v):
10        if not v[0].isupper():
11            raise ValueError('Nazwa musi zaczynać się wielką literą')
12        return v
13
14    @field_validator('population')
15    @classmethod
16    def population_realistic(cls, v):
17        if v > 1_000_000:
18            raise ValueError('Populacja przekracza realistyczny zakres')
19        return v
20
21# Walidacja działa
22lion = Species(name="Lew", population=120)  # OK
23
24try:
25    lion = Species(name="lew", population=120)
26except ValidationError as e:
27    print(e.errors()[0]["msg"])  # Value error, Nazwa musi zaczynać się wielką literą

Twój ValueError Pydantic opakowuje w ValidationError z przedrostkiem "Value error". Dokumentacja dopisuje @classmethod, bo walidator działa, zanim powstanie obiekt.

Safari API z Pydantic

Na koniec model trafia do FastAPI. Wspólne pola zbieramy w SpeciesBase, a osobne klasy opisują wejście i odpowiedź:

1from fastapi import FastAPI, HTTPException
2from pydantic import BaseModel, ConfigDict, Field, field_validator
3from typing import Literal
4from datetime import datetime
5
6app = FastAPI()
7
8class SpeciesBase(BaseModel):
9    name: str = Field(..., min_length=2, max_length=100)
10    scientific_name: str = Field(..., pattern=r'^[A-Z][a-z]+ [a-z]+$')
11    population: int = Field(..., ge=0, le=1_000_000)
12    habitat: Literal["savanna", "forest", "desert", "wetland"]
13
14    @field_validator('scientific_name')
15    @classmethod
16    def validate_scientific_name(cls, v):
17        parts = v.split()
18        if len(parts) != 2:
19            raise ValueError('Nazwa naukowa musi mieć format: Genus species')
20        return v
21
22class SpeciesCreate(SpeciesBase):
23    pass
24
25class Species(SpeciesBase):
26    id: int
27    created_at: datetime = Field(default_factory=datetime.now)
28
29    model_config = ConfigDict(from_attributes=True)
30
31@app.post("/species", response_model=Species)
32async def create_species(species: SpeciesCreate):
33    # Pydantic automatycznie waliduje dane!
34    new_species = Species(id=1, **species.model_dump())
35    return new_species

Gdy klient wyśle "habitat": "ocean", FastAPI odpowie kodem 422, a create_species w ogóle się nie wykona. Starsza zagnieżdżona class Config ustąpiła w Pydantic 2 miejsca model_config, a from_attributes=True przyda się przy bazie danych.

Podsumowanie

  • BaseModel - klasa bazowa, model_dump() i model_dump_json()
  • Pola opcjonalne: Optional[str] = None
  • Automatyczna konwersja typów
  • Field validators (gt, ge, pattern, etc.)
  • Custom validators (@field_validator)
  • Safari API z Pydantic

Następna lekcja: Async databases z SQLAlchemy!

Pamiętaj: model Pydantic to strażnik przy bramie parku - wpuszcza tylko dane o właściwym kształcie.

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Czym jest Pydantic?

  2. 2. Po jakiej klasie dziedziczy model Pydantic?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w pionie

    Ułóż definicję modelu Pydantic dla gatunku Safari:

  • Układanie w poziomie

    Jak zdefiniować opcjonalne pole w Pydantic?

  • Klikanie w kolejności

    Ułóż pole z walidatorem Field:

  • Edytor kodu

    Napisz model Species z polami: name (str), population (int > 0), habitat (str), endangered (bool, domyślnie False).

Przydatne artykuły