Neden önemli?
Anadolu Market'in web formundan, mobil uygulamasından ve tedarikçilerin gönderdiği CSV dosyalarından her gün binlerce kayıt gelir. Bunların bir kısmında fiyat "12,50" diye virgüllü, adet "üç" diye yazıyla, e-posta "ayse@" diye yarım gelir. Bu veriyi doğrulamadan veritabanına yazarsan hata günler sonra raporda "toplam ciro: NaN" olarak ortaya çıkar ve kaynağını bulmak saatler sürer. Kural basit: dışarıdan gelen her veri, sisteme girdiği kapıda doğrulanır.
dataclass ve tip ipuçları (type hint) dersinde gördüğün gibi Python tip ipuçlarını çalışma zamanında denetlemez: adet: int yazan bir dataclass'a "üç" verirsen sessizce kabul eder. pydantic tam bu boşluğu doldurur: tip ipuçlarını okuyup veriyi doğrular (validation), gerekirse dönüştürür (coercion/parsing) ve sorunları okunur bir hata listesiyle bildirir. FastAPI, LangChain, Prefect gibi popüler araçların veri katmanı pydantic'tir; Python iş ilanlarında en çok aranan paketlerden biridir. Bu derste pydantic v2 kullanıyoruz (kurulum: pip install pydantic; tarayıcıdaki motor da hazır yükler).
İlk model: BaseModel
Bir modeli sınıf olarak tanımlarsın; alanlar tip ipuçlarıyla yazılır. Nesne oluşturulurken tüm alanlar denetlenir.
from pydantic import BaseModel
class Urun(BaseModel):
ad: str
fiyat: float
stok: int = 0 # varsayılanı olan alan zorunlu değildir
u = Urun(ad="Türk kahvesi", fiyat="89.90", stok="12")
print(u)
print(u.fiyat + 10, type(u.stok).__name__)
print(Urun(ad="Çay", fiyat=45))
"89.90" metni float'a, "12" metni int'e çevrildi. Buna tip zorlama (type coercion) denir ve pydantic'in varsayılan "esnek" (lax) modudur: form ve CSV'den gelen her şey metin olduğu için çok işe yarar. Ama dönüşüm anlamlı olmalıdır: "on iki" ya da 12.5 (kesirli) bir int alana girmez.
| Alan tipi | Kabul edilen girdi | Reddedilen girdi |
|---|---|---|
int | 5, "5", 5.0 | "5,0", 5.5, "beş" |
float | 12.5, "12.5", 12 | "12,5" (virgül), "abc" |
bool | True, "true", "yes", 1, "0" | "evet", 2 |
date | "2026-10-26", date(...) | "26.10.2026" |
str | "metin" | 123 (v2 sayıyı metne çevirmez) |
model_config = ConfigDict(strict=True) ya da tek alan için Field(strict=True) kullan. Örneğin para transferi API'sinde "100" metnini sayı saymak istemeyebilirsin.ValidationError'ı okumak
Doğrulama başarısız olursa pydantic ilk hatada durmaz; tüm alanları dener ve hepsini tek bir ValidationError içinde toplar. e.errors() her hata için bir sözlük verir: loc (hatanın yeri, demet), type (makine için kısa kod), msg (insan için açıklama), input (gelen değer).
from pydantic import BaseModel, Field, ValidationError
class Urun(BaseModel):
ad: str = Field(min_length=2)
fiyat: float = Field(gt=0)
stok: int = 0
try:
Urun(ad="X", fiyat=-5, stok="çok")
except ValidationError as e:
print("hata sayısı:", e.error_count())
for hata in e.errors():
alan = ".".join(str(p) for p in hata["loc"])
print(f"{alan:6} | {hata['type']:18} | girdi={hata['input']!r}")
Programın içinde karar verirken type alanını kullan (sürümler arasında sabittir); kullanıcıya gösterirken msg'yi kendi Türkçe metninle eşleştir. Sık görülen tipler: missing (alan yok), int_parsing, float_parsing, greater_than, less_than_equal, string_too_short, string_too_long, string_pattern_mismatch, extra_forbidden, value_error (senin validator'ının fırlattığı hata).
Field: kısıtlar, varsayılanlar, takma adlar
Field bir alana tipten fazlasını söyler. En sık kullanılanlar:
| Parametre | Anlamı | Örnek |
|---|---|---|
gt, ge, lt, le | Sayı sınırları (büyük, büyük-eşit...) | Field(gt=0, le=100_000) |
min_length, max_length | Metin ya da liste uzunluğu | Field(max_length=50) |
pattern | Düzenli ifade (regex) | Field(pattern=r"^AM-\d{4}$") |
default, default_factory | Varsayılan değer / üretici | Field(default_factory=list) |
alias | Dış dünyadaki ad farklıysa | Field(alias="musteriNo") |
description | Belge ve JSON şeması için açıklama | Field(description="TL") |
from pydantic import BaseModel, Field, ConfigDict
class Musteri(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
musteri_no: int = Field(alias="musteriNo", gt=0)
ad: str = Field(min_length=2, max_length=50)
email: str = Field(pattern=r"^[\w.+-]+@[\w-]+\.[\w.]+$")
etiketler: list[str] = Field(default_factory=list)
m = Musteri.model_validate({"musteriNo": "1042", "ad": " Ayşe Kaya ", "email": "ayse@ornek.com"})
print(m)
print(m.model_dump(by_alias=True))
ConfigDict model genelinde ayar yapar: str_strip_whitespace=True metinlerin baş/son boşluğunu kırpar, extra="forbid" modelde olmayan bir alan gelirse hata verir (yazım hatalı alan adlarını yakalar; varsayılan "ignore" sessizce atar).
Optional ve varsayılanlar: ince fark
v2'de "None olabilir" ile "verilmeyebilir" iki ayrı şeydir:
| Yazım | Alan verilmezse | None verilirse |
|---|---|---|
kupon: str | Hata (missing) | Hata |
kupon: str | None | Hata (missing): zorunlu ama None olabilir | Kabul |
kupon: str | None = None | None olur | Kabul |
kupon: str = "YOK" | "YOK" olur | Hata |
fiyat: float = Field(0, gt=0) yazarsan fiyat verilmediğinde 0 olur ve gt=0 kuralı uygulanmaz; pydantic varsayılanları güvenilir kabul eder. Zorunlu bir alana "yanlışlıkla" varsayılan vermek, eksik veriyi sessizce sisteme sokmanın en yaygın yoludur.Enum, Literal ve iç içe modeller
Bir alan sabit bir küme içinden değer alıyorsa Enum ya da Literal kullan. Gerçek veri neredeyse hiç düz değildir: siparişin içinde kalemler, kalemin içinde ürün vardır. Bir modeli başka modelin alan tipi olarak kullanırsın; hatanın loc'u tam yolu gösterir. Dikkat: Literal tip zorlama yapmaz; Literal[12, 24] alanına "12" metni girmez, Enum ise değeri eşleşen metni üyeye çevirir.
from enum import Enum
from typing import Literal
from pydantic import BaseModel, Field, ValidationError
class Durum(str, Enum):
HAZIRLANIYOR = "hazirlaniyor"
KARGODA = "kargoda"
TESLIM = "teslim"
class Kalem(BaseModel):
urun: str
adet: int = Field(gt=0)
birim_fiyat: float = Field(gt=0)
class Siparis(BaseModel):
no: str
durum: Durum = Durum.HAZIRLANIYOR
kargo: Literal["standart", "hizli"] = "standart"
kalemler: list[Kalem] = Field(min_length=1)
s = Siparis(no="AM-1001", durum="kargoda",
kalemler=[{"urun": "Çay", "adet": 2, "birim_fiyat": 45}])
print(s.durum, s.durum is Durum.KARGODA, s.kalemler[0].adet)
try:
Siparis(no="AM-1002", kargo="uçak",
kalemler=[{"urun": "Çay", "adet": 1, "birim_fiyat": 45},
{"urun": "Kahve", "adet": 0, "birim_fiyat": 90}])
except ValidationError as e:
for h in e.errors():
print(h["loc"], h["type"])
Validator'lar: kendi kuralların
Tip ve Field kısıtları yetmediğinde kendi kuralını yazarsın. İki tür vardır:
@field_validator("alan"): tek bir alanı denetler ya da dönüştürür.mode="after"(varsayılan) tip dönüşümünden sonra çalışır, değer artık doğru tiptedir.mode="before"ham girdiyi alır; "12,50" gibi metni sayıya çevirmeden önce düzeltmek için idealdir.@model_validator(mode="after"): tüm alanlar doğrulandıktan sonra çalışır, alanlar arası kuralları (başlangıç < bitiş, taksit <= gelir) denetler;selfdöndürmelidir.
from datetime import date
from pydantic import BaseModel, field_validator, model_validator, ValidationError
class KampanyaFiyati(BaseModel):
urun: str
fiyat: float
baslangic: date
bitis: date
@field_validator("fiyat", mode="before")
@classmethod
def virgulu_noktaya_cevir(cls, v):
if isinstance(v, str):
v = v.replace(".", "").replace(",", ".") # "1.249,90" -> "1249.90"
return v
@field_validator("urun")
@classmethod
def urun_bos_olamaz(cls, v):
if not v.strip():
raise ValueError("ürün adı boş olamaz")
return v.strip() # DÖNDÜRMEYİ UNUTMA
@model_validator(mode="after")
def tarih_sirasi(self):
if self.bitis < self.baslangic:
raise ValueError("bitiş başlangıçtan önce olamaz")
return self
k = KampanyaFiyati(urun=" Filtre kahve ", fiyat="1.249,90", baslangic="2026-11-01", bitis="2026-11-30")
print(k.urun, k.fiyat)
try:
KampanyaFiyati(urun="Çay", fiyat="45", baslangic="2026-11-30", bitis="2026-11-01")
except ValidationError as e:
print(e.errors()[0]["type"], "|", e.errors()[0]["msg"])
Validator içinde ValueError fırlatırsan pydantic onu value_error tipinde bir hataya çevirir ve mesajın başına "Value error, " ekler. Validator'ın döndürdüğü değer alanın yeni değeridir; return unutulursa alan None olur.
Serileştirme: model_dump, model_dump_json, model_validate
Doğrulanmış modeli dış dünyaya geri vermek de önemlidir: JSON API cevabı, veritabanı satırı, log kaydı.
| Metot | Ne yapar |
|---|---|
Model.model_validate(sozluk) | Sözlükten model (v1: parse_obj) |
Model.model_validate_json(metin) | JSON metninden doğrudan model; tek adımda ayrıştırır ve doğrular |
m.model_dump() | Python sözlüğü (v1: dict()); Enum ve date nesne kalır |
m.model_dump(mode="json") | JSON uyumlu sözlük: Enum değere, date metne döner |
m.model_dump_json() | JSON metni (v1: json()) |
include / exclude / exclude_none / by_alias | Çıktıya hangi alanların gireceğini seçer |
m.model_copy(update={...}) | Kopya; update doğrulanmaz |
from datetime import date
from pydantic import BaseModel
class Hesap(BaseModel):
iban: str
sahip: str
bakiye: float
acilis: date
not_: str | None = None
h = Hesap.model_validate_json('{"iban": "TR120006200000000123456789", "sahip": "Ali Er", "bakiye": "1500.5", "acilis": "2024-03-01"}')
print(h.acilis.year, h.bakiye)
print(h.model_dump(exclude={"iban"}))
print(h.model_dump(mode="json", exclude_none=True))
print(h.model_dump_json(include={"sahip", "bakiye"}))
Ayarlar: pydantic-settings
Uygulama ayarlarını (SMTP sunucusu, port, dry-run bayrağı) koda gömmek yerine ortam değişkenlerinden ve .env dosyasından okumak standarttır. pydantic-settings bunu model gibi yapar: tipler zorlanır, eksik zorunlu ayar program başlarken hata verir. Bu paket tarayıcı motorunda yok; kendi bilgisayarında pip install pydantic-settings ile kur.
from pydantic import Field, SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Ayarlar(BaseSettings):
model_config = SettingsConfigDict(env_prefix="AM_", env_file=".env")
smtp_host: str = "localhost"
smtp_port: int = 587
smtp_sifre: SecretStr # zorunlu; print edilince ********** görünür
dry_run: bool = True
alicilar: list[str] = Field(default_factory=list) # ortamda JSON: '["a@x.com"]'
ayar = Ayarlar() # AM_SMTP_PORT=465, AM_DRY_RUN=false ... okunur
print(ayar.smtp_port, ayar.dry_run, ayar.smtp_sifre)
SecretStr değeri loglara ve ekrana yanlışlıkla basılmasını engeller; gerçek değere .get_secret_value() ile bilerek erişirsin.
dataclass mı, pydantic mi?
| Ölçüt | dataclass | pydantic BaseModel |
|---|---|---|
| Çalışma zamanında tip denetimi | Yok | Var, dönüştürerek |
| Hız | Çok hızlı (sıradan sınıf) | Hızlı (çekirdek Rust ile yazılmış) ama ek maliyetli |
| Bağımlılık | Standart kütüphane | Ek paket |
| JSON'a/JSON'dan | Elle (asdict + json) | Hazır, iç içe modellerle |
| Ne zaman? | Programın içinde dolaşan, zaten güvenilir veri | Sistemin sınırı: API, form, dosya, ayar |
Ara yol: from pydantic.dataclasses import dataclass dataclass sözdizimiyle doğrulama yapar; TypeAdapter(list[int]).validate_python(veri) ise model tanımlamadan herhangi bir tipi doğrular.
loc bu yüzden önemlidir). Aynı model, gece çalışan toplu içe aktarma betiğinde de kullanılır: tek bir kural, tek bir yerde.v1'den v2'ye geçiş
İnternetteki örneklerin çoğu hâlâ v1 sözdizimi kullanır. Eşleştirme: @validator → @field_validator, @root_validator → @model_validator, .dict() → .model_dump(), .json() → .model_dump_json(), parse_obj → model_validate, parse_raw → model_validate_json, class Config: → model_config = ConfigDict(...), regex= → pattern=. Eski adlar çoğunlukla uyarıyla çalışır ama yeni kodda kullanma.
Hesaplanan alanlar ve değişmezlik
@computed_field ile işaretlenen bir @property (ör. sipariş toplamı) model_dump çıktısına da girer. ConfigDict(frozen=True) modeli değiştirilemez ve hash'lenebilir yapar. validate_assignment=True ise nesne oluştuktan sonraki atamaları da doğrular.
JSON şeması ve sözleşme
Model.model_json_schema() modelden JSON Schema üretir. FastAPI'nin otomatik API belgesi buradan gelir; ön yüz ekibiyle "veri sözleşmesi" paylaşmanın en temiz yoludur.
Performans
pydantic v2'nin çekirdeği (pydantic-core) Rust ile yazılmıştır; v1'e göre kat kat hızlıdır. Yine de milyonlarca satırda satır satır model oluşturmak yavaştır: toplu veride önce pandas/polars ile vektörel denetim, yalnızca şüpheli satırlarda model kullanmak ya da TypeAdapter(list[Model]) ile tek çağrıda doğrulamak tercih edilir.