Bab 12 — Penanganan Error: Program yang Tidak Gampang Runtuh
Seri B3 — Program Kecil yang Berguna
Semua skrip bab ini ada di folder kode/bab12/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Bayangkan petugas lapangan mengetik “tiga puluh” padahal komputer menunggu angka 30. Tanpa persiapan, program berhenti mendadak dengan pesan merah yang menakutkan. Di bab-bab sebelumnya, error datang sebagai pelajaran. Sekarang Anda belajar bersiap menghadapinya, seperti petani memasang pagar di tepi kolam: bukan berharap tidak ada yang tersandung, melainkan memastikan tidak ada yang tercebur.
Dalam Python, error disebut exception (pengecualian). Saat ia terjadi dan tidak ada yang menangkapnya, program berhenti. Tugas kita: menangkap yang bisa diperbaiki, dan membiarkan yang memang fatal tampil jelas.

try dan except
01_try_dasar.py
# Bab 12 - try dan except: menangkap error agar program tidak runtuh
teks_diameter = "tiga puluh" # petugas mengetik kata, bukan angka
try:
diameter = float(teks_diameter)
print("Diameter:", diameter)
except ValueError:
print("Diameter harus berupa angka, bukan:", teks_diameter)
print("Program tetap berjalan sampai baris terakhir")Diameter harus berupa angka, bukan: tiga puluh
Program tetap berjalan sampai baris terakhir
Python mencoba isi try. Saat float("tiga puluh") gagal dengan ValueError, loncat ke except ValueError. Sisa isi try dilewati, tetapi program berlanjut: baris terakhir tetap tercetak.
Jangan menulis except: kosong atau menangkap semua dengan except Exception: lalu diam saja. Error nyata akan tersembunyi dan Anda tidak tahu mengapa hasilnya aneh. Tangkap jenis error yang Anda perkirakan, dan lakukan sesuatu yang berarti (beri pesan, catat, atau pakai nilai cadangan).
Kenali jenis error
Tiap error punya jenis dan pesan. Skrip berikut sengaja membuat delapan error umum dan menampilkannya:
02_jenis_error.py
# Bab 12 - Jenis error yang paling sering muncul
def coba(nama, tindakan):
try:
tindakan()
except Exception as e:
print(f"{nama:14} -> {type(e).__name__}: {e}")
daftar = [10, 20, 30]
data = {"jenis": "Jati"}
coba("ValueError", lambda: int("tiga"))
coba("ZeroDivision", lambda: 10 / 0)
coba("IndexError", lambda: daftar[5])
coba("KeyError", lambda: data["diameter"])
coba("TypeError", lambda: "umur: " + 5)
coba("NameError", lambda: nama_tak_ada)
coba("AttributeError", lambda: daftar.tambah(40))
coba("FileNotFound", lambda: open("tidak_ada.txt"))ValueError -> ValueError: invalid literal for int() with base 10: 'tiga'
ZeroDivision -> ZeroDivisionError: division by zero
IndexError -> IndexError: list index out of range
KeyError -> KeyError: 'diameter'
TypeError -> TypeError: can only concatenate str (not "int") to str
NameError -> NameError: name 'nama_tak_ada' is not defined
AttributeError -> AttributeError: 'list' object has no attribute 'tambah'
FileNotFound -> FileNotFoundError: [Errno 2] No such file or directory: 'tidak_ada.txt'
| Jenis | Artinya | Contoh penyebab di lapangan |
|---|---|---|
ValueError |
nilai salah untuk tipe itu | mengubah “tiga” jadi angka |
ZeroDivisionError |
dibagi nol | rata-rata dari daftar kosong |
IndexError |
nomor urut di luar daftar | mengambil plot ke-6 padahal ada 3 |
KeyError |
kunci dict tidak ada | meminta kolom “diameter” yang tidak ada |
TypeError |
tipe tidak cocok | menggabung teks dengan angka |
NameError |
nama variabel belum ada | salah ketik nama variabel |
AttributeError |
objek tidak punya fitur itu | memanggil .tambah() pada list |
FileNotFoundError |
berkas tidak ditemukan | salah nama berkas (lihat Bab 13) |
Pada skrip itu, except Exception as e dipakai hanya sebagai alat peraga, supaya semua jenis bisa ditampilkan dengan type(e).__name__ (nama jenisnya) dan e (pesannya).
else dan finally
03_else_finally.py
# Bab 12 - try / except / else / finally
def bagi(a, b):
try:
hasil = a / b
except ZeroDivisionError:
print(" [except] pembagi nol")
return None
else:
print(" [else] tidak ada error")
return hasil
finally:
print(" [finally] selalu dijalankan")
print("bagi(10, 4):")
print(" hasil =", bagi(10, 4))
print("bagi(10, 0):")
print(" hasil =", bagi(10, 0))bagi(10, 4):
[else] tidak ada error
[finally] selalu dijalankan
hasil = 2.5
bagi(10, 0):
[except] pembagi nol
[finally] selalu dijalankan
hasil = None
elsedijalankan hanya bila tidak ada error ditry.finallydijalankan selalu, ada error atau tidak, bahkan saat adareturn. Cocok untuk “bersih-bersih” seperti menutup berkas.
Perhatikan bagi(10, 4): urutannya else lalu finally. Pada bagi(10, 0): except lalu finally.
raise: memunculkan error sendiri
Kadang tidak ada error bawaan, tetapi data jelas tidak masuk akal. pH air hanya mungkin 0 sampai 14. Kita sendiri yang memunculkannya dengan raise:
04_raise.py
# Bab 12 - raise: kita sendiri yang memunculkan error
def cek_ph(ph):
if not 0 <= ph <= 14:
raise ValueError(f"pH {ph} di luar rentang 0-14")
return ph
for nilai in [7.2, 15, -1]:
try:
print("pH sah:", cek_ph(nilai))
except ValueError as e:
print("Ditolak:", e)pH sah: 7.2
Ditolak: pH 15 di luar rentang 0-14
Ditolak: pH -1 di luar rentang 0-14
pH 7,2 lolos; 15 dan -1 ditolak dengan pesan yang kita tulis. Pesan error yang jelas menghemat banyak waktu: sebut apa yang salah dan nilainya.
Jenis error buatan sendiri
Untuk proyek besar, buatlah jenis error sendiri dengan mewarisi Exception (kelas dan pewarisan dibahas penuh di Bab 16; di sini cukup ikuti polanya):
05_error_sendiri.py
# Bab 12 - Membuat jenis error sendiri
class DataLapanganError(Exception):
"""Induk semua error data lapangan kita."""
class DiameterTidakWajar(DataLapanganError):
def __init__(self, nilai):
super().__init__(f"diameter {nilai} cm tidak wajar (harus 1 sampai 300)")
self.nilai = nilai
def periksa_diameter(cm):
if not 1 <= cm <= 300:
raise DiameterTidakWajar(cm)
return cm
for cm in [32.5, 0.2, 450]:
try:
print("OK:", periksa_diameter(cm))
except DataLapanganError as e: # menangkap semua keturunan DataLapanganError
print(f"{type(e).__name__}: {e} (nilai={e.nilai})")OK: 32.5
DiameterTidakWajar: diameter 0.2 cm tidak wajar (harus 1 sampai 300) (nilai=0.2)
DiameterTidakWajar: diameter 450 cm tidak wajar (harus 1 sampai 300) (nilai=450)
DiameterTidakWajar adalah turunan dari DataLapanganError, sehingga except DataLapanganError menangkap keduanya. Berguna: satu except bisa menangani seluruh keluarga error proyek kita, sementara error bawaan lain tetap lewat.
Membaca traceback
Bila error tidak tertangkap, Python mencetak traceback: jejak urutan pemanggilan fungsi sampai ke titik error.
06_baca_traceback.py
# Bab 12 - Membaca traceback: error tiga lapis fungsi
def rata_rata(angka):
return sum(angka) / len(angka)
def ringkas_plot(nama, diameter):
rata = rata_rata(diameter)
print(f"Plot {nama}: rata-rata {rata:.1f} cm")
def laporan():
ringkas_plot("A", [32.5, 45.2])
ringkas_plot("B", []) # plot B belum ada datanya
laporan()Plot A: rata-rata 38.9 cm
Traceback (most recent call last):
File "06_baca_traceback.py", line 16, in <module>
laporan()
~~~~~~~^^
File "06_baca_traceback.py", line 13, in laporan
ringkas_plot("B", []) # plot B belum ada datanya
~~~~~~~~~~~~^^^^^^^^^
File "06_baca_traceback.py", line 7, in ringkas_plot
rata = rata_rata(diameter)
File "06_baca_traceback.py", line 3, in rata_rata
return sum(angka) / len(angka)
~~~~~~~~~~~^~~~~~~~~~~~
ZeroDivisionError: division by zero
Bacalah dari bawah ke atas:
- Baris terakhir adalah inti:
ZeroDivisionError: division by zero, yaitu jenis dan pesan error. - Baris
File ... in rata_ratatepat di atasnya menunjukkan tempat kejadian: pembagiansum(angka) / len(angka)denganlen(angka)bernilai 0. - Ke atas lagi,
ringkas_plotyang memanggilnya, lalulaporan, lalu<module>(kode tingkat paling luar). Itulah rantai pemanggilan. - Tanda
~~~^^^di bawah baris kode menunjuk bagian ekspresi yang bermasalah. - Sebab sebenarnya ada di data: plot B daftarnya kosong. Plot A tercetak sebelum error, jadi jejaknya juga memberi tahu sampai mana program sempat jalan.
Nomor baris pada traceback dihitung dari baris pertama berkas, termasuk baris komentar di paling atas. Karena itu nomor yang tampil di atas cocok persis dengan baris pada kode di buku dan pada berkas unduhan: hitung baris komentar pembuka sebagai baris 1.
Mencari kutu: print, assert, logging
Kesalahan program disebut bug (kutu). Mencarinya disebut debugging. Tiga alat dasar:
1. print debugging: cetak isi variabel di tiap langkah. Pakai repr() agar tanda kutip dan spasi tersembunyi tampak.
07_print_debug.py
# Bab 12 - Debugging dengan print: lihat isi variabel di tiap langkah
def bobot_total_kg(kolam):
total = 0
for k in kolam:
bobot_kg = k["jumlah"] * k["bobot_g"] / 1000
print(" [debug]", repr(k["nama"]), "->", bobot_kg) # repr menampakkan tanda kutip
total += bobot_kg
return total
kolam = [
{"nama": "A", "jumlah": 800, "bobot_g": 540.5},
{"nama": "B", "jumlah": 1250, "bobot_g": 300.0},
]
print("Total:", bobot_total_kg(kolam), "kg") [debug] 'A' -> 432.4
[debug] 'B' -> 375.0
Total: 807.4 kg
Kolam A 432,4 kg dan kolam B 375,0 kg, jumlahnya 807,4 kg. Dengan baris [debug] Anda melihat tiap suku penjumlahannya. Hapus baris itu setelah bug ketemu.
2. assert: pagar pengaman yang menyatakan “ini pasti benar”. Bila salah, program berhenti dengan AssertionError.
08_assert_lolos.py
# Bab 12 - assert: pagar pengaman "ini PASTI benar" (versi yang lolos)
def persen_hidup(awal, hidup):
assert awal > 0, "jumlah awal harus lebih dari 0"
assert 0 <= hidup <= awal, "ikan hidup tidak boleh lebih banyak dari awal"
return hidup / awal * 100
print(f"{persen_hidup(1000, 940):.1f}%")94.0%
09_assert_gagal.py
# Bab 12 - assert yang gagal: asumsi kita ternyata keliru
def persen_hidup(awal, hidup):
assert awal > 0, "jumlah awal harus lebih dari 0"
assert 0 <= hidup <= awal, "ikan hidup tidak boleh lebih banyak dari awal"
return hidup / awal * 100
print(f"{persen_hidup(1000, 1040):.1f}%")Traceback (most recent call last):
File "09_assert_gagal.py", line 8, in <module>
print(f"{persen_hidup(1000, 1040):.1f}%")
~~~~~~~~~~~~^^^^^^^^^^^^
File "09_assert_gagal.py", line 4, in persen_hidup
assert 0 <= hidup <= awal, "ikan hidup tidak boleh lebih banyak dari awal"
^^^^^^^^^^^^^^^^^^
AssertionError: ikan hidup tidak boleh lebih banyak dari awal
Ikan hidup (1040) lebih banyak daripada ikan awal (1000), tidak masuk akal, maka assert kedua menghentikan program dengan pesan yang kita tulis. Gunakan assert untuk menangkap kesalahan programmer, bukan kesalahan input pengguna.
Python dapat dijalankan dengan opsi -O yang membuang semua assert. Kami mencobanya pada skrip di atas: dengan python -O, hasilnya tercetak 104.0% tanpa error. Jadi jangan memakai assert sebagai satu-satunya pemeriksa input dari pengguna; pakai if dan raise.
3. logging: print yang lebih rapi, dengan tingkat kepentingan (DEBUG, INFO, WARNING, ERROR, CRITICAL) yang bisa disaring.
10_logging_awal.py
# Bab 12 - logging: catatan kerja program dengan tingkat kepentingan
import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG,
format="%(levelname)-8s %(message)s")
logging.debug("memeriksa 3 baris data")
logging.info("data kolam A dibaca")
logging.warning("pH kolam B = 9.8, terlalu tinggi")
logging.error("data kolam C tidak ditemukan")
logging.getLogger().setLevel(logging.WARNING) # sekarang hanya WARNING ke atas
logging.info("pesan ini tidak muncul")
logging.warning("pesan ini muncul")DEBUG memeriksa 3 baris data
INFO data kolam A dibaca
WARNING pH kolam B = 9.8, terlalu tinggi
ERROR data kolam C tidak ditemukan
WARNING pesan ini muncul
Setelah tingkat dinaikkan ke WARNING, pesan info tidak muncul lagi, tetapi warning tetap. Dengan logging, Anda tidak perlu menghapus pesan debug saat program selesai; cukup naikkan tingkatnya. (Pembahasan logging lebih jauh ada di Bab 19.)
Alat keempat adalah debugger pdb bawaan Python: fungsi breakpoint() menghentikan program di satu baris dan membuka prompt (Pdb) tempat Anda bisa mengetik p variabel (lihat isi), n (baris berikut), c (lanjut), dan q (keluar).
11_pdb_debugger.py
# Bab 12 - Debugger interaktif pdb: berhenti di satu baris dan mengintip isi variabel
def rata_rata(angka):
total = sum(angka)
breakpoint() # program berhenti di sini dan membuka prompt (Pdb)
return total / len(angka)
print(rata_rata([32.5, 45.2, 28.0]))Kode ini tidak dijalankan saat buku dibuat karena pdb menunggu ketikan pengguna, jadi keluarannya tidak ditampilkan. Coba sendiri di terminal: jalankan berkasnya, lalu ketik p total dan c.
Proyek Bersambung B3: tahan terhadap input salah
Dua skrip proyek. Pertama, meminta input sampai benar. Masukan tiruan (disediakan berkas .input) berisi lima jawaban: abc, -5, baris kosong, 450, dan akhirnya 32,5.
12_input_aman.py
# Bab 12 - Proyek B3: meminta angka sampai benar (tahan terhadap input salah)
def minta_diameter():
while True:
teks = input("Diameter pohon (cm): ")
try:
nilai = float(teks.replace(",", ".")) # terima "32,5" dan "32.5"
except ValueError:
print(" Bukan angka, coba lagi.")
continue
if not 1 <= nilai <= 300:
print(" Harus antara 1 dan 300 cm, coba lagi.")
continue
return nilai
hasil = minta_diameter()
print("Diameter tercatat:", hasil)Diameter pohon (cm): abc
Bukan angka, coba lagi.
Diameter pohon (cm): -5
Harus antara 1 dan 300 cm, coba lagi.
Diameter pohon (cm):
Bukan angka, coba lagi.
Diameter pohon (cm): 450
Harus antara 1 dan 300 cm, coba lagi.
Diameter pohon (cm): 32,5
Diameter tercatat: 32.5
Empat jawaban pertama ditolak dengan alasan berbeda (bukan angka, di luar rentang), dan jawaban 32,5 diterima setelah koma diganti titik. Pola while True + try/except + continue + return adalah resep standar “ulangi sampai benar”.
Kedua, membersihkan baris data campur aduk yang kelak datang dari berkas (Bab 13 dan 14). Ada enam baris; tiga di antaranya rusak:
13_proyek_tahan_salah.py
# Bab 12 - Proyek B3: membersihkan baris data yang campur aduk tanpa runtuh
import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.WARNING, format="%(levelname)s: %(message)s")
class BarisSalah(Exception):
pass
def baca_baris(teks):
"""Mengubah 'plot,jenis,diameter' menjadi dict, atau munculkan BarisSalah."""
bagian = [b.strip() for b in teks.split(",")]
if len(bagian) != 3:
raise BarisSalah(f"harus 3 kolom, ada {len(bagian)}: {teks!r}")
plot, jenis, diameter = bagian
try:
diameter = float(diameter.replace(",", "."))
except ValueError:
raise BarisSalah(f"diameter bukan angka: {diameter!r}") from None
if not 1 <= diameter <= 300:
raise BarisSalah(f"diameter di luar 1-300: {diameter}")
return {"plot": plot, "jenis": jenis, "diameter_cm": diameter}
mentah = ["A,Jati,32.5", "A,Meranti,45.2", "B,Mahoni,dua puluh", "B,Sengon", "C,Jati,-3", "C,Jati,36.0"]
bersih, ditolak = [], 0
for baris in mentah:
try:
bersih.append(baca_baris(baris))
except BarisSalah as e:
logging.warning("baris ditolak: %s", e)
ditolak += 1
print(f"Diterima {len(bersih)} baris, ditolak {ditolak}")
for b in bersih:
print(b)WARNING: baris ditolak: diameter bukan angka: 'dua puluh'
WARNING: baris ditolak: harus 3 kolom, ada 2: 'B,Sengon'
WARNING: baris ditolak: diameter di luar 1-300: -3.0
Diterima 3 baris, ditolak 3
{'plot': 'A', 'jenis': 'Jati', 'diameter_cm': 32.5}
{'plot': 'A', 'jenis': 'Meranti', 'diameter_cm': 45.2}
{'plot': 'C', 'jenis': 'Jati', 'diameter_cm': 36.0}
Alurnya: baca_baris memunculkan BarisSalah untuk tiap jenis kerusakan; perulangan menangkapnya, mencatat peringatan dengan logging, dan lanjut ke baris berikutnya. Hasil akhir: 3 baris diterima, 3 ditolak, dan program tidak pernah runtuh. Bagian from None menyembunyikan error asli (ValueError) supaya pesan kita yang tampil; tanpa itu, traceback akan memuat dua error yang membingungkan pembaca.
Latihan
- Tulis fungsi
ke_angka(teks)yang mengembalikanfloatdari teks, atauNonebila teksnya bukan angka. Uji dengan"12,5","abc", dan"". - Ubah
13_proyek_tahan_salah.pysupaya diameter di atas 150 cm tidak ditolak, tetapi hanya menghasilkanlogging.warning("pohon sangat besar"). - Buat
assertpada fungsirata_rata(angka)yang memastikan daftar tidak kosong. Bandingkan pesan errornya denganZeroDivisionErrordi traceback tadi. - Tantangan: buat kelas error
PhTidakWajar(DataLapanganError)dan fungsiperiksa_kolam(daftar)yang mengumpulkan semua kolam bermasalah (bukan berhenti di yang pertama), lalu mencetak ringkasan.
Ringkasan Bab
try/exceptmenangkap error;elsejalan bila tidak ada error;finallyselalu jalan.- Tangkap jenis error yang spesifik; hindari
except:kosong. raisememunculkan error sendiri; jenis error baru dibuat dengan mewarisiException.- Traceback dibaca dari bawah: jenis dan pesan, tempat kejadian, lalu rantai pemanggilan.
- Alat debugging:
print(denganrepr),assert(bisa dimatikan dengan-O),logging, danpdb/breakpoint(). - Proyek B3: program meminta input sampai benar dan menolak baris data rusak tanpa runtuh.
Selanjutnya, di Bab 13: file dan I/O dasar, cara program menyimpan dan membaca data agar tidak hilang saat ditutup.