Pydantic to najpopularniejsza biblioteka do walidacji danych w Pythonie. Wykorzystuje type hints do definiowania schematow danych, automatycznej walidacji i serializacji. Jest fundamentem wielu frameworkow AI — w tym LangChain, CrewAI, FastAPI i OpenAI SDK. Jesli pracujesz z agentami AI, musisz znac Pydantic.
- Podstawy Pydantic: BaseModel i Field
- Walidacja danych i typy
- Walidatory: @field_validator i @model_validator
- Serializacja i deserializacja (JSON, dict)
- Nested models i dziedziczenie
- Settings management
- Integracja z frameworkami AI
Dlaczego Pydantic?
Python jest dynamicznie typowany — co oznacza, ze bledy typow pojawiaja sie dopiero w runtime. Pydantic rozwiazuje ten problem:
✅ Walidacja
Automatyczna walidacja typow i wartosci
🔧 Serializacja
Latwa konwersja do/z JSON, dict
📝 Dokumentacja
Automatyczne JSON Schema
🚀 Wydajnosc
Core w Rust (Pydantic V2)
Instalacja
# Podstawowa instalacja
pip install pydantic
# Z dodatkami (email-validator, etc.)
pip install pydantic[email]
# Pydantic settings (dla konfiguracji)
pip install pydantic-settings
# Sprawdz wersje (powinna byc 2.x)
python -c "import pydantic; print(pydantic.VERSION)"
Ten artykul dotyczy Pydantic V2 (wydany w 2023). V2 jest ~5-50x szybszy dzieki core w Rust. Jesli masz stary kod z V1, sprawdz migration guide.
Podstawy: BaseModel
Kazdy model Pydantic dziedziczy po BaseModel:
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
# Definicja modelu
class User(BaseModel):
id: int
name: str
email: str
age: Optional[int] = None # pole opcjonalne z domyslna wartoscia
is_active: bool = True # pole z domyslna wartoscia
created_at: datetime = datetime.now()
# Tworzenie instancji - dane sa WALIDOWANE
user = User(
id=1,
name="Jan Kowalski",
email="jan@example.com"
)
print(user)
# id=1 name='Jan Kowalski' email='jan@example.com' age=None is_active=True ...
print(user.name) # Jan Kowalski
print(user.model_dump()) # {'id': 1, 'name': 'Jan Kowalski', ...}
Automatyczna konwersja typow
# Pydantic automatycznie konwertuje typy gdy to mozliwe
user = User(
id="123", # string -> int ✅
name="Anna",
email="anna@example.com",
age="25", # string -> int ✅
is_active="true" # string -> bool ✅
)
print(user.id) # 123 (int, nie string!)
print(type(user.id)) # <class 'int'>
Bledy walidacji
from pydantic import ValidationError
try:
user = User(
id="not-a-number", # nie da sie skonwertowac na int
name="Test",
email="test@example.com"
)
except ValidationError as e:
print(e)
# 1 validation error for User
# id
# Input should be a valid integer [type=int_parsing, ...]
# Szczegoly bledow jako lista
print(e.errors())
# [{'type': 'int_parsing', 'loc': ('id',), 'msg': '...', 'input': 'not-a-number'}]
Field — szczegolowa konfiguracja pol
Field pozwala dodac walidacje, opisy i ograniczenia:
from pydantic import BaseModel, Field
from typing import Optional
class Product(BaseModel):
# Pole wymagane z opisem
name: str = Field(
..., # ... oznacza "wymagane"
min_length=1,
max_length=100,
description="Nazwa produktu"
)
# Pole z ograniczeniami numerycznymi
price: float = Field(
...,
gt=0, # greater than 0
le=100000, # less or equal 100000
description="Cena w PLN"
)
# Pole z domyslna wartoscia i aliasem
quantity: int = Field(
default=0,
ge=0, # greater or equal 0
alias="qty" # alternatywna nazwa w JSON
)
# Pole opcjonalne
description: Optional[str] = Field(
default=None,
max_length=1000
)
# Pole z wzorcem regex
sku: str = Field(
...,
pattern=r'^[A-Z]{3}-\d{4}$', # np. ABC-1234
description="Kod produktu (format: ABC-1234)"
)
# Poprawne uzycie
product = Product(
name="Laptop Dell",
price=4599.99,
qty=10, # uzywamy aliasu
sku="LAP-0001"
)
# Blad - cena ujemna
try:
Product(name="Test", price=-10, sku="TST-0001")
except ValidationError as e:
print(e) # Input should be greater than 0
Ograniczenia Field
Typy danych
Pydantic wspiera wszystkie standardowe typy Pythona oraz dodaje wlasne:
from pydantic import (
BaseModel,
EmailStr, # walidowany email
HttpUrl, # walidowany URL
PositiveInt, # int > 0
PositiveFloat, # float > 0
NegativeInt, # int < 0
NonNegativeInt, # int >= 0
constr, # constrained string
conint, # constrained int
confloat, # constrained float
conlist, # constrained list
)
from datetime import datetime, date
from typing import List, Dict, Optional, Literal, Union
from enum import Enum
from uuid import UUID
class Status(str, Enum):
PENDING = "pending"
ACTIVE = "active"
COMPLETED = "completed"
class CompleteModel(BaseModel):
# Podstawowe typy
name: str
count: int
price: float
is_valid: bool
# Typy Pydantic
email: EmailStr # walidowany email
website: HttpUrl # walidowany URL
quantity: PositiveInt # musi byc > 0
# Constrained types
code: constr(min_length=3, max_length=10, to_upper=True)
rating: confloat(ge=0, le=5)
tags: conlist(str, min_length=1, max_length=10)
# Datetime
created_at: datetime
birth_date: date
# Kolekcje
items: List[str]
metadata: Dict[str, str]
# Enum
status: Status
# Literal - tylko te wartosci
priority: Literal["low", "medium", "high"]
# Union - jeden z typow
value: Union[int, str]
# UUID
id: UUID
# Optional
description: Optional[str] = None
# Przyklad uzycia
from uuid import uuid4
model = CompleteModel(
name="Test",
count=10,
price=99.99,
is_valid=True,
email="test@example.com",
website="https://example.com",
quantity=5,
code="abc", # zostanie zamienione na "ABC"
rating=4.5,
tags=["python", "pydantic"],
created_at=datetime.now(),
birth_date="1990-01-15", # string -> date
items=["item1", "item2"],
metadata={"key": "value"},
status="active", # string -> Status enum
priority="high",
value=42,
id=uuid4()
)
Walidatory
Walidatory pozwalaja na niestandardowa logike walidacji:
@field_validator — walidacja pojedynczego pola
from pydantic import BaseModel, field_validator
class User(BaseModel):
name: str
email: str
age: int
password: str
# Walidator dla jednego pola
@field_validator('name')
@classmethod
def name_must_not_be_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError('Imie nie moze byc puste')
return v.strip().title() # transformacja: "jan kowalski" -> "Jan Kowalski"
# Walidator z mode='before' - przed konwersja typow
@field_validator('email', mode='before')
@classmethod
def email_to_lowercase(cls, v):
if isinstance(v, str):
return v.lower().strip()
return v
# Walidator dla wielu pol naraz
@field_validator('age')
@classmethod
def age_must_be_valid(cls, v: int) -> int:
if v < 0 or v > 150:
raise ValueError('Wiek musi byc miedzy 0 a 150')
return v
# Walidator hasla
@field_validator('password')
@classmethod
def password_strength(cls, v: str) -> str:
if len(v) < 8:
raise ValueError('Haslo musi miec min. 8 znakow')
if not any(c.isupper() for c in v):
raise ValueError('Haslo musi zawierac wielka litere')
if not any(c.isdigit() for c in v):
raise ValueError('Haslo musi zawierac cyfre')
return v
# Test
user = User(
name=" jan kowalski ",
email="JAN@EXAMPLE.COM",
age=30,
password="Haslo123"
)
print(user.name) # "Jan Kowalski"
print(user.email) # "jan@example.com"
@model_validator — walidacja calego modelu
from pydantic import BaseModel, model_validator
from typing import Self
class DateRange(BaseModel):
start_date: date
end_date: date
# Walidator PRZED utworzeniem instancji
@model_validator(mode='before')
@classmethod
def check_dates_exist(cls, data: dict):
if 'start_date' not in data or 'end_date' not in data:
raise ValueError('Obie daty sa wymagane')
return data
# Walidator PO utworzeniu instancji
@model_validator(mode='after')
def check_date_order(self) -> Self:
if self.end_date < self.start_date:
raise ValueError('end_date musi byc po start_date')
return self
class UserRegistration(BaseModel):
password: str
password_confirm: str
@model_validator(mode='after')
def passwords_match(self) -> Self:
if self.password != self.password_confirm:
raise ValueError('Hasla musza byc identyczne')
return self
# Test
try:
reg = UserRegistration(password="abc123", password_confirm="xyz789")
except ValidationError as e:
print(e) # Hasla musza byc identyczne
Serializacja i deserializacja
from pydantic import BaseModel
from datetime import datetime
class Event(BaseModel):
name: str
date: datetime
attendees: int
event = Event(name="Konferencja", date=datetime.now(), attendees=100)
# === Do dict ===
data = event.model_dump()
print(data)
# {'name': 'Konferencja', 'date': datetime.datetime(...), 'attendees': 100}
# Z wybranymi polami
data = event.model_dump(include={'name', 'date'})
data = event.model_dump(exclude={'attendees'})
# === Do JSON ===
json_str = event.model_dump_json()
print(json_str)
# '{"name":"Konferencja","date":"2025-01-12T...","attendees":100}'
# Z formatowaniem
json_str = event.model_dump_json(indent=2)
# === Z dict ===
data = {"name": "Meetup", "date": "2025-06-15T18:00:00", "attendees": 50}
event = Event.model_validate(data)
# lub
event = Event(**data)
# === Z JSON ===
json_str = '{"name": "Workshop", "date": "2025-07-20T10:00:00", "attendees": 25}'
event = Event.model_validate_json(json_str)
# === JSON Schema ===
schema = Event.model_json_schema()
print(schema)
# {'properties': {'name': {'title': 'Name', 'type': 'string'}, ...}}
Nested Models
Modele moga zawierac inne modele:
from pydantic import BaseModel
from typing import List, Optional
class Address(BaseModel):
street: str
city: str
zip_code: str
country: str = "Polska"
class Company(BaseModel):
name: str
nip: str
class Person(BaseModel):
name: str
email: str
address: Address # nested model
company: Optional[Company] = None # optional nested
tags: List[str] = []
# Tworzenie z nested dict - Pydantic automatycznie tworzy nested models
person = Person(
name="Jan",
email="jan@example.com",
address={
"street": "ul. Marszalkowska 1",
"city": "Warszawa",
"zip_code": "00-001"
},
company={
"name": "Firma XYZ",
"nip": "1234567890"
}
)
print(person.address.city) # Warszawa
print(person.company.name) # Firma XYZ
# Lub z instancjami
address = Address(street="ul. Dluga 5", city="Krakow", zip_code="30-001")
person2 = Person(name="Anna", email="anna@example.com", address=address)
Dziedziczenie
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
# Model bazowy
class BaseEntity(BaseModel):
id: int
created_at: datetime = datetime.now()
updated_at: Optional[datetime] = None
# Dziedziczenie
class User(BaseEntity):
name: str
email: str
class Product(BaseEntity):
name: str
price: float
# User ma: id, created_at, updated_at, name, email
user = User(id=1, name="Jan", email="jan@example.com")
# Product ma: id, created_at, updated_at, name, price
product = Product(id=1, name="Laptop", price=4999.99)
Settings Management
Pydantic Settings to idealny sposob na zarzadzanie konfiguracja aplikacji:
# pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
from typing import Optional
class Settings(BaseSettings):
"""Konfiguracja aplikacji z env variables."""
# Automatycznie czytane z env
app_name: str = "My App"
debug: bool = False
# API keys
openai_api_key: str = Field(..., description="OpenAI API Key")
anthropic_api_key: Optional[str] = None
# Database
database_url: str = "sqlite:///./app.db"
db_pool_size: int = 5
# Server
host: str = "0.0.0.0"
port: int = 8000
# Konfiguracja
model_config = SettingsConfigDict(
env_file=".env", # czytaj z pliku .env
env_file_encoding="utf-8",
case_sensitive=False, # OPENAI_API_KEY = openai_api_key
extra="ignore" # ignoruj nieznane env vars
)
# Uzycie
settings = Settings()
print(settings.openai_api_key)
print(settings.database_url)
# Plik .env:
# OPENAI_API_KEY=sk-...
# DEBUG=true
# DATABASE_URL=postgresql://user:pass@localhost/db
Integracja z frameworkami AI
Pydantic jest fundamentem wielu frameworkow AI:
OpenAI Structured Output
from pydantic import BaseModel
from openai import OpenAI
class MovieReview(BaseModel):
title: str
rating: float
summary: str
pros: list[str]
cons: list[str]
client = OpenAI()
response = client.beta.chat.completions.parse(
model="gpt-4o",
messages=[{"role": "user", "content": "Review the movie Inception"}],
response_format=MovieReview # Pydantic model!
)
review = response.choices[0].message.parsed # MovieReview instance
print(review.title)
print(review.rating)
LangChain Output Parsers
from langchain_core.output_parsers import PydanticOutputParser
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
class TaskExtraction(BaseModel):
"""Extracted task from text."""
task: str = Field(description="The main task")
priority: str = Field(description="Priority: low, medium, high")
deadline: str = Field(description="Deadline if mentioned")
parser = PydanticOutputParser(pydantic_object=TaskExtraction)
llm = ChatOpenAI().with_structured_output(TaskExtraction)
result = llm.invoke("Muszę skończyć raport do piątku, to pilne!")
print(result.task) # "skończyć raport"
print(result.priority) # "high"
print(result.deadline) # "piątek"
CrewAI State
from crewai.flow.flow import Flow, start, listen
from pydantic import BaseModel
class ResearchState(BaseModel):
"""State for research flow."""
topic: str = ""
findings: list[str] = []
summary: str = ""
class ResearchFlow(Flow[ResearchState]): # Pydantic model jako state!
@start()
def init(self):
self.state.topic = "AI Agents"
return self.state.topic
FastAPI
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
description: str | None = None
@app.post("/items/")
async def create_item(item: Item): # automatyczna walidacja!
return {"item": item, "status": "created"}
# FastAPI automatycznie:
# - waliduje request body
# - generuje OpenAPI schema
# - tworzy dokumentacje Swagger
Konfiguracja modelu
from pydantic import BaseModel, ConfigDict
class StrictModel(BaseModel):
"""Model z customowa konfiguracja."""
model_config = ConfigDict(
# Walidacja
strict=True, # brak automatycznej konwersji typow
validate_assignment=True, # waliduj przy przypisaniu
# Pola
extra="forbid", # blad przy nieznanych polach
frozen=True, # immutable (jak dataclass frozen)
# Aliasy
populate_by_name=True, # akceptuj i alias i nazwa pola
# JSON
use_enum_values=True, # serializuj enum jako wartosc
# String
str_strip_whitespace=True, # strip() na stringach
str_min_length=1, # minimalna dlugosc stringow
)
name: str
value: int
# Strict mode - brak konwersji
try:
StrictModel(name="Test", value="123") # BLAD! string != int
except ValidationError:
print("Strict mode wymaga dokladnych typow")
Podsumowanie API
- Uzywaj strict mode w produkcji dla bezpieczenstwa
- Dodawaj opisy do Field() — trafiaja do JSON Schema i dokumentacji
- Preferuj model_validator(mode=’after’) — masz dostep do zwalidowanych pol
- Uzywaj BaseSettings do konfiguracji — laczy .env, env vars i defaults
- Testuj walidacje — pisz unit testy dla custom walidatorow
📚 Bibliografia
- Pydantic. (2025). Pydantic Documentation. docs.pydantic.dev
- Pydantic. (2025). Pydantic V2 Migration Guide. docs.pydantic.dev/latest/migration
- Pydantic. (2025). Pydantic Settings. docs.pydantic.dev/latest/concepts/pydantic_settings
- GitHub. (2025). Pydantic Repository. github.com/pydantic/pydantic
- FastAPI. (2025). Request Body with Pydantic. fastapi.tiangolo.com