Pipeline membaca data dari pihak lain, dan pihak lain tidak pernah menjamin kualitas. Satu baris dengan qty "tiga", satu berkas yang lupa dikirim, satu kolom yang hilang — tanpa penanganan, satu kejadian kecil menghentikan seluruh job batch yang memproses jutaan baris. Python memakai exception untuk melaporkan kegagalan, dan try/except memberi kita pilihan: berhenti total, lewati baris bermasalah, atau lanjut dengan nilai cadangan. Chapter ini membahas anatomi penanganan galat, exception bawaan yang paling sering muncul, custom exception DataValidationError, lalu mengujinya pada baris CSV kotor.
Anatomi try, except, else, finally #
Keempat klausul bekerja sebagai satu kesatuan. Tabel berikut merangkum kapan masing-masing dijalankan; rujukan resminya ada di https://docs.python.org/3/tutorial/errors.html .
| Klausul | Kapan dijalankan | Pemakaian khas di pipeline |
|---|---|---|
try | selalu lebih dulu | kode yang berisiko gagal |
except | saat galat cocok dengan tipenya | catat, lewati baris, atau pakai nilai cadangan |
else | saat try selesai tanpa galat | lanjutkan pemrosesan hasil |
finally | selalu, termasuk setelah return | rapikan resource, catat akhir baris |
Klausul else menjaga blok try tetap pendek, sehingga galat yang tertangkap benar-benar berasal dari kode berisiko. Klausul finally berjalan walau except melakukan return — tempat pencatatan “baris selesai” tanpa peduli hasilnya.
Menangkap Exception Spesifik #
Tangkap exception yang paling spesifik lebih dulu. Blok except ValueError menyatakan kegagalan apa yang diantisipasi, sehingga kegagalan lain tetap terdengar sebagai galat sungguhan. Tiga exception berikut menutup sebagian besar kasus ETL:
try:
int("tiga")
except ValueError as galat:
print("ValueError :", galat)
record = {"id": "TRX-001"}
try:
record["harga"]
except KeyError as galat:
print("KeyError :", galat)
try:
open("tidak_ada.csv", encoding="utf-8")
except FileNotFoundError as galat:
print("FileNotFoundError:", galat)Output yang Dihasilkan:
ValueError : invalid literal for int() with base 10: 'tiga'
KeyError : 'harga'
FileNotFoundError: [Errno 2] No such file or directory: 'tidak_ada.csv'ValueError muncul saat konversi nilai gagal, KeyError saat kolom tidak ada di dict, dan FileNotFoundError saat berkas sumber tidak ditemukan. Ketiganya membawa pesan siap diteruskan ke log.
raise dan Custom Exception DataValidationError #
Kata kunci raise menolak data lebih awal dengan pesan yang jelas, alih-alih membiarkan nilai rusak mengalir ke tabel hasil. Custom exception melangkah lebih jauh: class DataValidationError(Exception) mendefinisikan jenis galat milik pipeline kita sendiri, sehingga pemanggil bisa membedakan “baris melanggar aturan bisnis” dari “ada bug di dalam program”.
Konvensinya sama seperti exception bawaan: akhiri nama class dengan Error. Badan boleh kosong karena fungsinya sebagai penanda; stack trace tetap berisi nomor baris dan pesan lengkap. Pada studi kasus di bawah, DataValidationError dipakai untuk dua aturan: jumlah kolom dan nilai positif.
Studi Kasus: Parsing Baris CSV Kotor #
Lima baris mentah berikut memuat tiga jenis kerusakan: qty bukan angka, harga negatif, dan jumlah kolom kurang. Skrip parsing.py menolak baris tersebut tanpa menghentikan loop:
class DataValidationError(Exception):
"""Galat khusus: baris tidak lolos validasi struktur atau aturan bisnis."""
def parse_baris(nomor, teks):
"""Ubah satu baris CSV mentah menjadi record, atau raise galat."""
kolom = [k.strip() for k in teks.split(",")]
if len(kolom) != 4:
raise DataValidationError(
f"baris {nomor}: jumlah kolom {len(kolom)}, harus 4"
)
id_trx, kasir, qty, harga = kolom
try:
qty_i = int(qty)
harga_i = int(harga)
except ValueError:
raise ValueError(
f"baris {nomor}: qty/harga bukan angka ({qty!r}/{harga!r})"
) from None
if qty_i <= 0 or harga_i <= 0:
raise DataValidationError(
f"baris {nomor}: qty/harga harus positif ({qty_i}/{harga_i})"
)
return {
"id": id_trx,
"kasir": kasir.title(),
"qty": qty_i,
"harga": harga_i,
"total": qty_i * harga_i,
}
baris_mentah = [
"TRX-001, Budi, 2, 24000",
"TRX-002, Siti, tiga, 15000",
"TRX-003, Dewi, 1, -5000",
"TRX-004, Budi, 1",
"TRX-005, Siti, 4, 32000",
]
bersih = []
gagal = []
for nomor, teks in enumerate(baris_mentah, start=1):
try:
record = parse_baris(nomor, teks)
except (ValueError, DataValidationError) as galat:
print(f"baris {nomor:2d} DITOLAK : {galat}")
gagal.append(nomor)
else:
print(f"baris {nomor:2d} LOLOS : {record['id']} total Rp{record['total']}")
bersih.append(record)
finally:
print(f" baris {nomor} selesai diproses")
print()
print("Baris lolos:", len(bersih))
print("Baris gagal :", len(gagal))
print("Total valid :", "Rp" + str(sum(r["total"] for r in bersih)))python3 parsing.pyOutput yang Dihasilkan:
baris 1 LOLOS : TRX-001 total Rp48000
baris 1 selesai diproses
baris 2 DITOLAK : baris 2: qty/harga bukan angka ('tiga'/'15000')
baris 2 selesai diproses
baris 3 DITOLAK : baris 3: qty/harga harus positif (1/-5000)
baris 3 selesai diproses
baris 4 DITOLAK : baris 4: jumlah kolom 3, harus 4
baris 4 selesai diproses
baris 5 LOLOS : TRX-005 total Rp128000
baris 5 selesai diproses
Baris lolos: 2
Baris gagal : 3
Total valid : Rp176000Mari kita telaah baris-per-baris kode di atas:
- Baris 1-2 (
class DataValidationError): class tersebut mewarisiException; badannya cukup docstring karena perannya sebagai penanda, bukan tempat logika. - Baris 7-11 (
raise):parse_barismemeriksa jumlah kolom terhadap schema; pelanggaran langsung ditolak dengan pesan yang memuat nomor baris. - Baris 13-19 (
raise ... from None): saat"tiga"gagal dikonversi,ValueErrorasli diganti pesan berkonteks;from Nonemenyembunyikan jejak galat asli sehingga log hanya menampilkan pesan yang bermakna. - Baris 20-23 (aturan bisnis): nilai tidak positif ditolak sebagai
DataValidationError— aturan bisnis dibedakan dari galat konversi. - Baris 43-53 (loop utama):
exceptmenangkap dua jenis galat sekaligus;elsehanya berjalan setelahparse_barissukses;finallymencatat akhir setiap baris — baris yang ditolak pun tetap mendapat baris “selesai diproses”.
Pada output terlihat pola yang dicari operator: tiga baris ditolak dengan alasan berbeda, dua baris lolos, dan laporan akhir tetap tercetak. Pertanyaannya kemudian: bagaimana mencatat penolakan tersebut dengan tingkat keparahan dan waktu kejadian? Jawabannya adalah modul logging, dibahas di chapter berikutnya, Logging.