Ana içeriğe geç
Kesit Python
Orta · Otomasyon, web ve araçlar

Veri doğrulama ve pydantic v2

Dışarıdan gelen veriyi pydantic BaseModel ile doğrular ve dönüştürür; Field kısıtları, field/model validator'lar, iç içe modeller, Enum ve Optional ile kurallarını yazar, ValidationError'ı okur, model_dump ve model_validate ile JSON'a gidip gelirsin.

Uygulamada aç, 18 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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 tipiKabul edilen girdiReddedilen girdi
int5, "5", 5.0"5,0", 5.5, "beş"
float12.5, "12.5", 12"12,5" (virgül), "abc"
boolTrue, "true", "yes", 1, "0""evet", 2
date"2026-10-26", date(...)"26.10.2026"
str"metin"123 (v2 sayıyı metne çevirmez)
Katı modDönüşüm istemediğin yerde 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:

ParametreAnlamıÖrnek
gt, ge, lt, leSayı sınırları (büyük, büyük-eşit...)Field(gt=0, le=100_000)
min_length, max_lengthMetin ya da liste uzunluğuField(max_length=50)
patternDüzenli ifade (regex)Field(pattern=r"^AM-\d{4}$")
default, default_factoryVarsayılan değer / üreticiField(default_factory=list)
aliasDış dünyadaki ad farklıysaField(alias="musteriNo")
descriptionBelge ve JSON şeması için açıklamaField(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ımAlan verilmezseNone verilirse
kupon: strHata (missing)Hata
kupon: str | NoneHata (missing): zorunlu ama None olabilirKabul
kupon: str | None = NoneNone olurKabul
kupon: str = "YOK""YOK" olurHata
Varsayılan değer doğrulanmazfiyat: 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:

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ı.

MetotNe 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çütdataclasspydantic BaseModel
Çalışma zamanında tip denetimiYokVar, 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ıkStandart kütüphaneEk paket
JSON'a/JSON'danElle (asdict + json)Hazır, iç içe modellerle
Ne zaman?Programın içinde dolaşan, zaten güvenilir veriSistemin 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.

Gerçek işteBir bankanın kredi başvuru servisinde başvuru formu önce pydantic modeline girer; model_validator taksit/gelir oranını, field_validator TC kimlik numarası algoritmasını denetler. Hata listesi olduğu gibi 422 cevabına dönüştürülür ve ön yüz her hatayı ilgili form kutusunun altında gösterir (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.
Uzmanlaş

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.

Alıştırmalara geç (18)