Kurs Python · Moduł 6: Async i FastAPI
Pydantic - walidacja danych z Rust
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=NoneSamo 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 equallt,le- less than, less or equalmin_length,max_length- długość stringpattern- regex validationdescription- 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_speciesGdy 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()imodel_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. Czym jest Pydantic?
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).