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 .

KlausulKapan dijalankanPemakaian khas di pipeline
tryselalu lebih dulukode yang berisiko gagal
exceptsaat galat cocok dengan tipenyacatat, lewati baris, atau pakai nilai cadangan
elsesaat try selesai tanpa galatlanjutkan pemrosesan hasil
finallyselalu, termasuk setelah returnrapikan 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:

python
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:

text
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:

python
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)))
bash
python3 parsing.py

Output yang Dihasilkan:

text
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 : Rp176000

Mari kita telaah baris-per-baris kode di atas:

  1. Baris 1-2 (class DataValidationError): class tersebut mewarisi Exception; badannya cukup docstring karena perannya sebagai penanda, bukan tempat logika.
  2. Baris 7-11 (raise): parse_baris memeriksa jumlah kolom terhadap schema; pelanggaran langsung ditolak dengan pesan yang memuat nomor baris.
  3. Baris 13-19 (raise ... from None): saat "tiga" gagal dikonversi, ValueError asli diganti pesan berkonteks; from None menyembunyikan jejak galat asli sehingga log hanya menampilkan pesan yang bermakna.
  4. Baris 20-23 (aturan bisnis): nilai tidak positif ditolak sebagai DataValidationError — aturan bisnis dibedakan dari galat konversi.
  5. Baris 43-53 (loop utama): except menangkap dua jenis galat sekaligus; else hanya berjalan setelah parse_baris sukses; finally mencatat 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.