%%{init: {"flowchart": {"diagramPadding": 40}}}%%
flowchart LR
A["Data mentah<br/>(dict, JSON, .env)"] --> B["Model Pydantic<br/>(tipe + aturan)"]
B -- "lolos" --> C["Objek bersih<br/>dengan tipe benar"]
B -- "tidak lolos" --> D["ValidationError<br/>dengan pesan jelas"]
Bab 34 — Validasi Data dan Konfigurasi: Penjaga Pintu dengan Pydantic
Seri I4 — Bahasa Python Tingkat Lanjut
Semua skrip bab ini ada di folder kode/bab34/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Di Bab 33, type hint berfungsi sebagai label yang dibaca alat pemeriksa. Tetapi saat program berjalan, label itu tidak menjaga apa-apa. Padahal data lapangan sering datang dari luar: formulir, berkas CSV, JSON dari API, ketikan petugas. Isinya bisa salah ketik, kosong, atau di luar akal sehat (pH air 17, diameter pohon negatif).
Perlu penjaga pintu: pemeriksa yang bekerja saat program berjalan, menolak data buruk dengan pesan jelas, dan merapikan data yang masih bisa diterima. Di Python, penjaga pintu yang paling populer adalah Pydantic.
dataclass rapi, tetapi tidak menjaga
Di Bab 16 Anda mengenal dataclass: cara ringkas membuat kelas penyimpan data. Lihat apa yang terjadi bila isinya tidak masuk akal:
01_dataclass_tak_memeriksa.py
# Bab 34 - dataclass rapi, tetapi tidak memeriksa isi
from dataclasses import dataclass
@dataclass
class Pohon:
jenis: str
diameter_cm: float
sehat: bool
bagus = Pohon("Jati", 32.5, True)
print(bagus)
kacau = Pohon("Jati", "tiga puluh", "mungkin") # tidak ada protes
print(kacau)
print(type(kacau.diameter_cm).__name__)Pohon(jenis='Jati', diameter_cm=32.5, sehat=True)
Pohon(jenis='Jati', diameter_cm='tiga puluh', sehat='mungkin')
str
Objek kacau dibuat tanpa protes: diameter berisi teks 'tiga puluh', sehat berisi 'mungkin'. Tipenya str, padahal kita menulis float. dataclass hanya merapikan penulisan kelas, tidak memeriksa isi. Kesalahan baru terasa nanti, mungkin jauh dari sumbernya, ketika angka itu dihitung.
Model Pydantic
Pydantic adalah pustaka validasi data yang memakai type hint sebagai aturan. Versi yang terpasang di komputer penulis berseri utama 2 (skrip mencetak “versi utama” saja, karena nomor lengkapnya berubah dari waktu ke waktu). Buku ini memakai gaya Pydantic seri 2.
02_model_pydantic.py
# Bab 34 - Model Pydantic: memeriksa dan mengubah tipe
import pydantic
from pydantic import BaseModel
print("Pydantic versi utama:", pydantic.VERSION.split(".")[0])
class Pohon(BaseModel):
jenis: str
diameter_cm: float
sehat: bool = True # nilai bawaan
p = Pohon(jenis="Jati", diameter_cm="32.5", sehat="true") # teks diubah menjadi angka dan bool
print(p)
print(type(p.diameter_cm).__name__, type(p.sehat).__name__)
print(p.model_dump())
p2 = Pohon(jenis="Meranti", diameter_cm=45) # int menjadi float
print(p2.diameter_cm, p2.sehat)Pydantic versi utama: 2
jenis='Jati' diameter_cm=32.5 sehat=True
float bool
{'jenis': 'Jati', 'diameter_cm': 32.5, 'sehat': True}
45.0 True
- Model dibuat dengan kelas turunan
BaseModel, dan setiap atribut diberi tipe.sehat: bool = Truepunya nilai bawaan. - Pydantic mengonversi bila masuk akal: teks
"32.5"menjadifloat32.5, teks"true"menjadiTrue, dan bilangan bulat45menjadi45.0. - Hasilnya objek dengan tipe yang benar:
floatdanbool(bukanstr). model_dump()mengubah objek kembali menjadi dict biasa.
Di sini ada pelajaran kecil. Saat saya mencoba mengisi sehat="ya" (kata Indonesia untuk “ya”), Pydantic menolaknya (bool_parsing): ia tidak mengenal kata itu. Yang ia kenali adalah bentuk baku seperti "true". Konversi Pydantic itu terbatas dan terdokumentasi, bukan menebak.
Pesan galat yang jelas
Bila data tidak bisa diterima, Pydantic melempar ValidationError yang menjelaskan semua masalah sekaligus, bukan hanya yang pertama.
03_pesan_galat.py
# Bab 34 - Pesan galat yang jelas
from pydantic import BaseModel, ValidationError
class Pohon(BaseModel):
jenis: str
diameter_cm: float
sehat: bool
try:
Pohon(jenis="Jati", diameter_cm="tiga puluh", sehat="mungkin")
except ValidationError as e:
print("jumlah masalah:", e.error_count())
for masalah in e.errors(include_url=False, include_input=False):
print(masalah["loc"], "|", masalah["type"], "|", masalah["msg"])
try:
Pohon(jenis="Jati")
except ValidationError as e:
for masalah in e.errors(include_url=False, include_input=False):
print(masalah["loc"], "|", masalah["type"], "|", masalah["msg"])jumlah masalah: 2
('diameter_cm',) | float_parsing | Input should be a valid number, unable to parse string as a number
('sehat',) | bool_parsing | Input should be a valid boolean, unable to interpret input
('diameter_cm',) | missing | Field required
('sehat',) | missing | Field required
Pada percobaan pertama, ada dua masalah: diameter_cm tidak bisa dibaca sebagai angka (float_parsing) dan sehat tidak bisa dibaca sebagai ya/tidak (bool_parsing). Pada percobaan kedua, dua kolom wajib tidak diisi (missing: “Field required”). Setiap masalah punya:
loc: letak masalah (nama kolom),type: kategori galat yang stabil dan bisa diproses program,msg: kalimat penjelas untuk manusia.
Opsi include_url=False dan include_input=False menghilangkan tautan dan salinan masukan dari tampilan, supaya keluaran ringkas dan stabil.
Aturan tambahan: Field dan validator
Tipe saja belum cukup. “Angka” belum berarti “masuk akal”. Aturan tambahan bisa dipasang dengan Field (batas sederhana) dan validator (aturan buatan sendiri).
04_aturan_validasi.py
# Bab 34 - Aturan lewat Field dan validator
from typing import Literal
from pydantic import BaseModel, Field, ValidationError, field_validator, model_validator
class Ikan(BaseModel):
kolam: str = Field(min_length=1, max_length=3)
jenis: Literal["lele", "nila"]
jumlah: int = Field(gt=0)
bobot_rata_g: float = Field(ge=1, le=5000)
ph_air: float = Field(ge=0, le=14)
@field_validator("kolam")
@classmethod
def huruf_besar(cls, nilai: str) -> str:
return nilai.upper() # sekaligus merapikan
@model_validator(mode="after")
def periksa_bobot_lele(self):
if self.jenis == "lele" and self.bobot_rata_g > 2000:
raise ValueError("bobot rata-rata lele melebihi 2000 g tidak masuk akal")
return self
print(Ikan(kolam="a", jenis="lele", jumlah=800, bobot_rata_g=540.5, ph_air=7.2))
for data in [
dict(kolam="B", jenis="gurame", jumlah=100, bobot_rata_g=300, ph_air=7),
dict(kolam="B", jenis="nila", jumlah=0, bobot_rata_g=300, ph_air=15),
dict(kolam="C", jenis="lele", jumlah=10, bobot_rata_g=3000, ph_air=7),
]:
try:
Ikan(**data)
except ValidationError as e:
for m in e.errors(include_url=False, include_input=False):
print(m["loc"], "|", m["msg"])
print("--")kolam='A' jenis='lele' jumlah=800 bobot_rata_g=540.5 ph_air=7.2
('jenis',) | Input should be 'lele' or 'nila'
--
('jumlah',) | Input should be greater than 0
('ph_air',) | Input should be less than or equal to 14
--
() | Value error, bobot rata-rata lele melebihi 2000 g tidak masuk akal
--
Field(gt=0)berarti “lebih besar dari 0”,ge/leberarti “lebih besar sama dengan”/“lebih kecil sama dengan”, danmin_length/max_lengthmembatasi panjang teks.Literal["lele", "nila"]membatasi pilihan jenis ikan."gurame"ditolak: “Input should be ‘lele’ or ‘nila’”.@field_validator("kolam")memeriksa (dan boleh merapikan) satu kolom: nama kolam"a"menjadi"A".@model_validator(mode="after")memeriksa gabungan kolom setelah semuanya valid: lele dengan bobot rata-rata di atas 2000 g ditolak, dan lokasi galatnya()(kosong) karena masalahnya milik seluruh model, bukan satu kolom.
Batas seperti bobot_rata_g <= 5000 atau “lele tidak lebih dari 2000 g” di skrip ini hanyalah angka contoh untuk latihan, bukan anjuran budidaya. Tentukan batas sebenarnya bersama penyuluh atau pembudidaya berpengalaman.
Pengaturan aplikasi: variabel lingkungan dan .env
Aplikasi punya pengaturan yang berbeda di tiap komputer: lokasi berkas, batas unduhan, mode uji, kunci layanan. Menaruhnya di dalam kode merepotkan dan, untuk kunci rahasia, berbahaya. Praktik umum: simpan di variabel lingkungan (environment variable, pengaturan di luar program) atau di berkas .env (daftar NAMA=nilai), dan baca saat program mulai.
Paket pydantic-settings membaca pengaturan ke dalam kelas yang divalidasi seperti model Pydantic. Skrip ini memakai nilai tiruan (tidak ada rahasia sungguhan) dan menulis berkas .env contoh ke _tmp/.
05_pengaturan_env.py
# Bab 34 - Pengaturan aplikasi dari variabel lingkungan dan berkas .env (nilai tiruan)
import os
from pathlib import Path
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
folder = Path(__file__).parent / "_tmp"
folder.mkdir(exist_ok=True)
berkas_env = folder / "contoh.env"
berkas_env.write_text("LAPANGAN_NAMA_UNIT=Kolam Contoh\nLAPANGAN_TOKEN_API=token-tiruan-bukan-rahasia\n"
"LAPANGAN_BATAS_UNDUH=5\n", encoding="utf-8")
class Pengaturan(BaseSettings):
model_config = SettingsConfigDict(env_prefix="LAPANGAN_", env_file=berkas_env, env_file_encoding="utf-8")
nama_unit: str = "Tanpa nama"
batas_unduh: int = 10
token_api: SecretStr = SecretStr("")
mode_uji: bool = False
s = Pengaturan()
print("dari .env :", s.nama_unit, s.batas_unduh, s.mode_uji)
print("token (disamarkan):", s.token_api)
print("token (sengaja dibuka):", s.token_api.get_secret_value() == "token-tiruan-bukan-rahasia")
os.environ["LAPANGAN_BATAS_UNDUH"] = "25" # variabel lingkungan mengalahkan .env
os.environ["LAPANGAN_MODE_UJI"] = "true"
s2 = Pengaturan()
print("env mengalahkan .env:", s2.batas_unduh, s2.mode_uji)
print("argumen langsung paling kuat:", Pengaturan(batas_unduh=1).batas_unduh)dari .env : Kolam Contoh 5 False
token (disamarkan): **********
token (sengaja dibuka): True
env mengalahkan .env: 25 True
argumen langsung paling kuat: 1
env_prefix="LAPANGAN_"berarti pengaturanbatas_unduhdibaca dariLAPANGAN_BATAS_UNDUH. Awalan ini mencegah bentrok dengan variabel milik program lain.- Nilai dari
.envotomatis dikonversi:"5"menjadiint5. SecretStrmenyamarkan nilai saat dicetak (**********), supaya kunci tidak ikut bocor ke log. Nilai aslinya hanya terbuka lewatget_secret_value(), yang kita pakai di sini hanya untuk membandingkan.- Urutan kekuatan, sesuai dokumentasi pydantic-settings: argumen langsung > variabel lingkungan > berkas
.env> nilai bawaan. Skrip membuktikannya:.envmemberi 5, variabel lingkungan menimpanya menjadi 25, dan argumenPengaturan(batas_unduh=1)mengalahkan semuanya.
Berkas .env berisi pengaturan pribadi dan sering rahasia. Jangan simpan di Git (masukkan ke .gitignore, Bab 20), dan sediakan berkas contoh (.env.example) tanpa nilai rahasia. Di buku ini semua nilai tiruan.
JSON, daftar model, dan skema
06_json_dan_skema.py
# Bab 34 - JSON, daftar model, dan skema JSON
import json
from pydantic import BaseModel, TypeAdapter
class Rekaman(BaseModel):
sektor: str
unit: str
nilai: float
mentah = '{"sektor": "kolam", "unit": "A", "nilai": "540.5"}'
r = Rekaman.model_validate_json(mentah) # JSON teks -> objek tervalidasi
print(r)
print(r.model_dump_json())
daftar = TypeAdapter(list[Rekaman]).validate_python([
{"sektor": "hutan", "unit": "H1", "nilai": 32.5},
{"sektor": "kebun", "unit": "K2", "nilai": "245.5"},
])
print(len(daftar), daftar[1].nilai)
skema = Rekaman.model_json_schema() # dasar dokumentasi API dan alat agent AI
print(json.dumps(skema, indent=2))sektor='kolam' unit='A' nilai=540.5
{"sektor":"kolam","unit":"A","nilai":540.5}
2 245.5
{
"properties": {
"sektor": {
"title": "Sektor",
"type": "string"
},
"unit": {
"title": "Unit",
"type": "string"
},
"nilai": {
"title": "Nilai",
"type": "number"
}
},
"required": [
"sektor",
"unit",
"nilai"
],
"title": "Rekaman",
"type": "object"
}
model_validate_jsonmemvalidasi teks JSON langsung ("540.5"menjadi angka).model_dump_json()mengubah objek kembali menjadi JSON.TypeAdapter(list[Rekaman])memvalidasi daftar model sekaligus.model_json_schema()menghasilkan skema JSON: deskripsi baku bentuk datanya (kolom, tipe, mana yang wajib). Skema inilah dasar dokumentasi otomatis sebuah API.
Dataclass atau Pydantic?
dataclass |
Pydantic BaseModel |
|
|---|---|---|
| Memeriksa isi saat berjalan | tidak | ya |
Mengonversi tipe ("32.5" ke 32,5) |
tidak | ya |
| Pesan galat terperinci | tidak | ya |
| Bagian pustaka standar | ya | tidak (pustaka tambahan) |
| Cocok untuk | data di dalam program yang sudah tepercaya | data yang masuk dari luar |
Aturan praktis: data yang melewati “perbatasan” (formulir, berkas, API, pengaturan) divalidasi dengan Pydantic; data internal yang sudah bersih boleh berupa dataclass.
Kaitan dengan FastAPI dan agent AI
Pemeriksaan di perbatasan ini menjadi dasar dua topik berikutnya dalam buku. FastAPI (Bab 36), kerangka kerja web, memakai Pydantic: paket FastAPI yang terpasang di komputer penulis mencantumkan pydantic sebagai ketergantungan (hasil pip show fastapi), dan di Bab 36 Anda akan melihat sendiri model Pydantic dipakai untuk memeriksa masukan API (di bab ini hal itu belum dijalankan). Pada pembahasan agent AI di Tingkat Advanced, skema serupa dipakai untuk menjelaskan bentuk data kepada model bahasa; bagian itu belum diuji di bab ini.
Proyek Bersambung I4: validasi data lapangan lima sektor
Data lapangan lima sektor punya bentuk berbeda: pohon punya diameter, kebun punya hasil, sawah punya gabah, kolam punya bobot dan pH, kandang punya bobot ternak. Kita buat lima model, lalu satu “pintu” yang memilih model dengan membaca kolom sektor (disebut discriminated union, gabungan bertanda).
07_proyek_validasi_lima_sektor.py
# Bab 34 - Proyek bersambung I4: validasi data lapangan lima sektor
from typing import Annotated, Literal
from pydantic import BaseModel, Field, TypeAdapter, ValidationError
class Hutan(BaseModel):
sektor: Literal["hutan"]
unit: str
diameter_cm: float = Field(gt=0, le=300)
class Kebun(BaseModel):
sektor: Literal["kebun"]
unit: str
hasil_kg: float = Field(ge=0)
class Sawah(BaseModel):
sektor: Literal["sawah"]
unit: str
gabah_kg: float = Field(ge=0)
class Kolam(BaseModel):
sektor: Literal["kolam"]
unit: str
bobot_rata_g: float = Field(gt=0)
ph_air: float = Field(ge=0, le=14)
class Kandang(BaseModel):
sektor: Literal["kandang"]
unit: str
bobot_kg: float = Field(gt=0)
Lapangan = Annotated[Hutan | Kebun | Sawah | Kolam | Kandang, Field(discriminator="sektor")]
pemeriksa = TypeAdapter(Lapangan)
mentah = [
{"sektor": "hutan", "unit": "H1", "diameter_cm": "32.5"},
{"sektor": "kebun", "unit": "K1", "hasil_kg": 245.5},
{"sektor": "sawah", "unit": "S1", "gabah_kg": -10},
{"sektor": "kolam", "unit": "A", "bobot_rata_g": 540.5, "ph_air": 7.2},
{"sektor": "kolam", "unit": "B", "bobot_rata_g": 498, "ph_air": 17},
{"sektor": "kandang", "unit": "D1", "bobot_kg": 412.5},
{"sektor": "empang", "unit": "E1", "bobot_rata_g": 100},
{"sektor": "kandang", "unit": "D2"},
]
sah, ditolak = [], []
for nomor, baris in enumerate(mentah, start=1):
try:
sah.append(pemeriksa.validate_python(baris))
except ValidationError as e:
pesan = e.errors(include_url=False, include_input=False)[0]
ditolak.append((nomor, ".".join(str(x) for x in pesan["loc"]) or "sektor", pesan["msg"]))
print("sah:", len(sah), "| ditolak:", len(ditolak))
print("jenis objek sah:", sorted({type(o).__name__ for o in sah}))
for nomor, tempat, alasan in ditolak:
print(f"baris {nomor}: {tempat} -> {alasan}")sah: 4 | ditolak: 4
jenis objek sah: ['Hutan', 'Kandang', 'Kebun', 'Kolam']
baris 3: sawah.gabah_kg -> Input should be greater than or equal to 0
baris 5: kolam.ph_air -> Input should be less than or equal to 14
baris 7: sektor -> Input tag 'empang' found using 'sektor' does not match any of the expected tags: 'hutan', 'kebun', 'sawah', 'kolam', 'kandang'
baris 8: kandang.bobot_kg -> Field required
Delapan baris mentah masuk, empat lolos dan empat ditolak:
- Baris 1:
diameter_cmberupa teks"32.5", dikonversi dan diterima sebagaiHutan. - Baris 3:
gabah_kgbernilai -10, ditolak karena harus>= 0. - Baris 5: pH 17, ditolak karena di atas 14.
- Baris 7: sektor
"empang"bukan salah satu dari lima, ditolak, dan pesannya menyebut pilihan yang sah. - Baris 8: kolom
bobot_kgtidak ada (“Field required”). - Jenis objek yang lolos:
Hutan,Kandang,Kebun,Kolam, masing-masing berbentuk berbeda tetapi semuanya sudah bertipe benar.
Data yang ditolak tidak membuat program berhenti: ia dicatat beserta alasannya, dan sisanya diproses. Itulah pola kerja yang aman untuk data lapangan yang kotor.
Latihan
- Buat model
Ternakdengan kolomkandang: str,jenis: Literal["sapi", "kambing", "ayam"],umur_bulan: int(harus>= 0), danbobot_kg: float(harus> 0). Coba dengan satu data benar dan satu data salah. - Ubah
03_pesan_galat.py: berikandiameter_cm=-5pada model yang punyaField(gt=0). Baca pesan galatnya. - Di
05_pengaturan_env.py, tambahkan pengaturanfolder_keluaran: strbernilai bawaan"hasil", lalu timpa lewat variabel lingkungan dan lewat.env. Amati mana yang menang. - Tantangan: tambahkan model keenam
Peralatanke07_proyek_validasi_lima_sektor.py(dengansektorbernilai"alat"), masukkan ke gabungan, dan tambahkan dua baris mentah (satu benar, satu salah). Hitung ulang jumlah lolos dan ditolak.
Ringkasan Bab
- Data dari luar harus divalidasi saat program berjalan; type hint saja tidak menjaga apa-apa.
dataclasstidak memeriksa isi; PydanticBaseModelmemeriksa, mengonversi, dan memberi pesan galat per kolom.- Aturan tambahan:
Field(batas),field_validator(satu kolom),model_validator(gabungan kolom). pydantic-settingsmembaca pengaturan dari variabel lingkungan dan.env; urutan kekuatan: argumen > lingkungan >.env> bawaan;SecretStrmenyamarkan rahasia.TypeAdapter,model_validate_json, danmodel_json_schemamemvalidasi daftar, JSON, dan menghasilkan skema.- Proyek I4: data lima sektor divalidasi lewat gabungan bertanda; empat lolos, empat ditolak dengan alasan.
Selanjutnya, di Bab 35: async dan concurrency, cara mengerjakan banyak pekerjaan menunggu sekaligus dan menutup Seri I4.