Bagian 3 menutup dengan satu kelemahan: muat_tugas percaya sepenuhnya bahwa tugas.json selalu sehat. Kenyataannya, berkas bisa terpotong karena listrik padam, tertimpa suntingan tangan yang keliru, atau berisi struktur yang tidak sesuai harapan. Pada bagian ini kita membangun pemuatan defensif — kode yang menuntaskan tiga kondisi bermasalah tanpa pernah menghentikan program dengan traceback.
Tiga Kondisi Bermasalah #
Mari kita petakan dulu kondisi apa saja yang mungkin dijumpai saat memuat, beserta keputusan penanganannya:
| Kondisi | Contoh isi berkas | Penanganan |
|---|---|---|
| Berkas hilang | tugas.json belum pernah dibuat | mulai dengan list kosong |
| JSON rusak | teks terpotong, tanda kutip hilang | pindahkan berkas sebagai cadangan, mulai kosong |
| Record tidak sah | id berupa string, kunci hilang | lewati record tersebut, pertahankan sisanya |
Kondisi pertama sudah ditangani pada Bagian 3. Dua sisanya kita bangun sekarang di storage.py.
Menangani JSON yang Rusak #
Teks yang bukan JSON valid membuat json.load() membangkitkan json.JSONDecodeError — subclass dari ValueError. Cabang except baru kita tangani dengan tiga langkah: beri tahu pengguna, pindahkan berkas bermasalah, lalu mulai dari daftar kosong:
except json.JSONDecodeError:
print(f"Berkas {nama_file} tidak berisi JSON yang valid.")
os.replace(nama_file, nama_file + ".bak")
print(f"Berkas yang rusak dipindah ke {nama_file}.bak.")
print("Program memulai dengan daftar tugas kosong.")
return []Dua keputusan di balik kode ini:
os.replace(nama_file, nama_file + ".bak"): fungsios.replace()dari moduleosmemindahkan berkas lama ke nama baru, menimpa.baksebelumnya bila ada. Tanpa pemindahan ini, penyimpanan pertama setelah kejadian akan menimpa berkas rusak dan bukti penyebabnya hilang selamanya; dengan cadangan.bak, pengguna masih bisa memeriksa isi berkas yang rusak itu.return [], bukanraise: program pengelola tugas harus tetap bisa dipakai meski datanya rusak. Mengembalikan list kosong memilih jalur “mulai dari awal” yang aman — pilihan yang wajar untuk aplikasi catatan pribadi, meski aplikasi kritis seperti pencatatan transaksi mungkin lebih memilih berhenti total daripada diam-diam kehilangan data.
Memvalidasi Setiap Record #
JSON yang berhasil di-parse belum tentu berbentuk list of dictionary. Oleh sebab itu, setiap record diperiksa satu per satu sebelum dipakai:
if not isinstance(data, list):
print(f"Isi {nama_file} tidak berupa daftar tugas.")
return []
daftar_sah = []
for item in data:
if not isinstance(item, dict):
continue
if not isinstance(item.get("id"), int) or isinstance(item.get("id"), bool):
continue
if not isinstance(item.get("judul"), str):
continue
if not isinstance(item.get("selesai"), bool):
continue
daftar_sah.append(item)
return daftar_sahMari kita bedah aturan validasinya:
if not isinstance(data, list):: JSON yang sah bisa berupa angka, string, atau object. Hanya list yang layak diproses; cabang ini menghentikan pemeriksaan lebih lanjut untuk bentuk lain sebelum loopforgagal karena mencoba mengiterasi sesuatu yang tidak iterable.if not isinstance(item, dict): continue: method.get()hanya dimiliki dictionary. Record seperti"bukan dictionary"dilewati pada pemeriksaan pertama.- Pemeriksaan per kunci dengan
item.get(...): methodget()mengembalikanNoneketika kunci tidak ada — danNonegagal di semua ujiisinstanceberikutnya, sehingga record dengan kunci hilang otomatis tersaring. or isinstance(item.get("id"), bool): pengaman khusus yang terlihat janggal. Di Python,booladalah subclass dariint, sehinggaisinstance(True, int)bernilaiTrue. Tanpa suku kedua, record dengan"id": trueakan lolos sebagai id1dan bertabrakan dengan tugas asli bernomor 1.continue, bukanraise: satu record rusak tidak boleh mengorbankan seluruh data. Record bermasalah dilewati diam-diam dan sisanya tetap dimuat.
Berikut storage.py lengkap setelah seluruh perubahan Bagian 4:
"""Modul penyimpanan daftar tugas ke berkas JSON."""
import json
import os
def simpan_tugas(daftar_tugas, nama_file):
with open(nama_file, "w", encoding="utf-8") as berkas:
json.dump(daftar_tugas, berkas, ensure_ascii=False, indent=2)
def muat_tugas(nama_file):
try:
with open(nama_file, "r", encoding="utf-8") as berkas:
data = json.load(berkas)
except FileNotFoundError:
return []
except json.JSONDecodeError:
print(f"Berkas {nama_file} tidak berisi JSON yang valid.")
os.replace(nama_file, nama_file + ".bak")
print(f"Berkas yang rusak dipindah ke {nama_file}.bak.")
print("Program memulai dengan daftar tugas kosong.")
return []
if not isinstance(data, list):
print(f"Isi {nama_file} tidak berupa daftar tugas.")
return []
daftar_sah = []
for item in data:
if not isinstance(item, dict):
continue
if not isinstance(item.get("id"), int) or isinstance(item.get("id"), bool):
continue
if not isinstance(item.get("judul"), str):
continue
if not isinstance(item.get("selesai"), bool):
continue
daftar_sah.append(item)
return daftar_sahPerubahan di main.py #
Satu tambahan kecil di main() membuat proses pemuatan terasa: program melaporkan berapa tugas yang berhasil dimuat. Berikut main.py lengkap setelah perubahan — seluruhnya identik dengan Bagian 3 kecuali dua baris baru di awal main():
"""Pengelola Daftar Tugas — program manajemen tugas berbasis terminal."""
import storage
NAMA_BERKAS = "tugas.json"
def tampilkan_menu():
print("=== Pengelola Daftar Tugas ===")
print("1. Tambah tugas")
print("2. Daftar tugas")
print("3. Tandai selesai")
print("4. Hapus tugas")
print("5. Keluar")
def nomor_id_baru(daftar_tugas):
if not daftar_tugas:
return 1
return max(tugas["id"] for tugas in daftar_tugas) + 1
def tambah_tugas(daftar_tugas, judul):
id_baru = nomor_id_baru(daftar_tugas)
tugas_baru = {"id": id_baru, "judul": judul, "selesai": False}
daftar_tugas.append(tugas_baru)
print(f"Tugas \"{judul}\" tersimpan dengan id {id_baru}.")
def tampilkan_daftar(daftar_tugas):
if not daftar_tugas:
print("Belum ada tugas.")
return
for tugas in daftar_tugas:
penanda = "[x]" if tugas["selesai"] else "[ ]"
print(f"{penanda} {tugas['id']}. {tugas['judul']}")
def baca_nomor():
try:
return int(input("Nomor tugas: ").strip())
except ValueError:
print("Nomor tugas harus berupa angka.")
return None
def tandai_selesai(daftar_tugas, nomor):
for tugas in daftar_tugas:
if tugas["id"] == nomor:
tugas["selesai"] = True
print(f"Tugas {nomor} ditandai selesai.")
return True
print(f"Tugas dengan nomor {nomor} tidak ditemukan.")
return False
def hapus_tugas(daftar_tugas, nomor):
for indeks, tugas in enumerate(daftar_tugas):
if tugas["id"] == nomor:
dihapus = daftar_tugas.pop(indeks)
print(f"Tugas \"{dihapus['judul']}\" dihapus.")
return True
print(f"Tugas dengan nomor {nomor} tidak ditemukan.")
return False
def main():
daftar_tugas = storage.muat_tugas(NAMA_BERKAS)
if daftar_tugas:
print(f"{len(daftar_tugas)} tugas dimuat dari {NAMA_BERKAS}.")
while True:
tampilkan_menu()
pilihan = input("Pilihan: ").strip()
if pilihan == "1":
judul = input("Judul tugas: ").strip()
if judul:
tambah_tugas(daftar_tugas, judul)
storage.simpan_tugas(daftar_tugas, NAMA_BERKAS)
else:
print("Judul tugas tidak boleh kosong.")
elif pilihan == "2":
tampilkan_daftar(daftar_tugas)
elif pilihan == "3":
nomor = baca_nomor()
if nomor is not None:
if tandai_selesai(daftar_tugas, nomor):
storage.simpan_tugas(daftar_tugas, NAMA_BERKAS)
elif pilihan == "4":
nomor = baca_nomor()
if nomor is not None:
if hapus_tugas(daftar_tugas, nomor):
storage.simpan_tugas(daftar_tugas, NAMA_BERKAS)
elif pilihan == "5":
print("Sampai jumpa!")
break
else:
print("Pilihan tidak dikenal.")
if __name__ == "__main__":
main()Pesan print(f"{len(daftar_tugas)} tugas dimuat dari {NAMA_BERKAS}.") sengaja hanya muncul ketika berkas berisi tugas — pada eksekusi pertama yang belum punya tugas.json, pengguna langsung disambut menu tanpa kabar “0 tugas” yang tidak informatif. Kedua berkas tahap ini — main.py dan storage.py yang dibahas di atas — tersedia utuh pada bagian Berkas Sumber di bawah halaman.
Sesi Uji: Berkas Bermasalah #
Untuk kondisi pertama, kita buat berkas yang jelas-jelas bukan JSON valid — tanda kutip pada kunci dan nilai hilang:
echo "{ id: 1, judul: rusak tanpa tanda kutip }" > tugas.json
echo "2
5" | python3 main.pyOutput yang Dihasilkan:
Berkas tugas.json tidak berisi JSON yang valid.
Berkas yang rusak dipindah ke tugas.json.bak.
Program memulai dengan daftar tugas kosong.
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: Belum ada tugas.
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: Sampai jumpa!Program tidak jatuh dengan traceback; pengguna justru mendapat tiga kabar jelas dan tetap bisa memakai program. Setelah sesi tersebut, isi folder memperlihatkan tugas.json.bak — bukti berkas rusak yang tersimpan.
Untuk kondisi kedua, ganti isi tugas.json dengan campuran record sah dan bermasalah:
[
{"id": 1, "judul": "Belajar Python dasar", "selesai": false},
{"id": "dua", "judul": "Id bertipe string", "selesai": false},
{"id": 3, "judul": 42, "selesai": false},
{"id": 4, "judul": "Tanpa kunci selesai"},
"bukan dictionary"
]Baris kedua gagal karena id berupa string, baris ketiga karena judul berupa angka, baris keempat karena kunci selesai tidak ada, dan baris kelima bukan dictionary. Hanya baris pertama yang sah:
echo "2
5" | python3 main.pyOutput yang Dihasilkan:
1 tugas dimuat dari tugas.json.
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: [ ] 1. Belajar Python dasar
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: Sampai jumpa!Empat record bermasalah tersaring diam-diam dan satu-satunya tugas yang sah tetap tampil. Pemuatan kini bertahan dari semua kondisi yang kita petakan di awal.
Pemuatan defensif telah selesai, dan program bekerja penuh. Namun struktur kodenya masih menyulitkan pengujian: logika operasi bercampur dengan print, dan validasi menyatu dengan pembacaan berkas. Bagian 5 merapikan semuanya menjadi fungsi kecil yang teruji.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
main.py
Unduh berkas"""Pengelola Daftar Tugas — program manajemen tugas berbasis terminal."""
import storage
NAMA_BERKAS = "tugas.json"
def tampilkan_menu():
print("=== Pengelola Daftar Tugas ===")
print("1. Tambah tugas")
print("2. Daftar tugas")
print("3. Tandai selesai")
print("4. Hapus tugas")
print("5. Keluar")
def nomor_id_baru(daftar_tugas):
if not daftar_tugas:
return 1
return max(tugas["id"] for tugas in daftar_tugas) + 1
def tambah_tugas(daftar_tugas, judul):
id_baru = nomor_id_baru(daftar_tugas)
tugas_baru = {"id": id_baru, "judul": judul, "selesai": False}
daftar_tugas.append(tugas_baru)
print(f"Tugas \"{judul}\" tersimpan dengan id {id_baru}.")
def tampilkan_daftar(daftar_tugas):
if not daftar_tugas:
print("Belum ada tugas.")
return
for tugas in daftar_tugas:
penanda = "[x]" if tugas["selesai"] else "[ ]"
print(f"{penanda} {tugas['id']}. {tugas['judul']}")
def baca_nomor():
try:
return int(input("Nomor tugas: ").strip())
except ValueError:
print("Nomor tugas harus berupa angka.")
return None
def tandai_selesai(daftar_tugas, nomor):
for tugas in daftar_tugas:
if tugas["id"] == nomor:
tugas["selesai"] = True
print(f"Tugas {nomor} ditandai selesai.")
return True
print(f"Tugas dengan nomor {nomor} tidak ditemukan.")
return False
def hapus_tugas(daftar_tugas, nomor):
for indeks, tugas in enumerate(daftar_tugas):
if tugas["id"] == nomor:
dihapus = daftar_tugas.pop(indeks)
print(f"Tugas \"{dihapus['judul']}\" dihapus.")
return True
print(f"Tugas dengan nomor {nomor} tidak ditemukan.")
return False
def main():
daftar_tugas = storage.muat_tugas(NAMA_BERKAS)
if daftar_tugas:
print(f"{len(daftar_tugas)} tugas dimuat dari {NAMA_BERKAS}.")
while True:
tampilkan_menu()
pilihan = input("Pilihan: ").strip()
if pilihan == "1":
judul = input("Judul tugas: ").strip()
if judul:
tambah_tugas(daftar_tugas, judul)
storage.simpan_tugas(daftar_tugas, NAMA_BERKAS)
else:
print("Judul tugas tidak boleh kosong.")
elif pilihan == "2":
tampilkan_daftar(daftar_tugas)
elif pilihan == "3":
nomor = baca_nomor()
if nomor is not None:
if tandai_selesai(daftar_tugas, nomor):
storage.simpan_tugas(daftar_tugas, NAMA_BERKAS)
elif pilihan == "4":
nomor = baca_nomor()
if nomor is not None:
if hapus_tugas(daftar_tugas, nomor):
storage.simpan_tugas(daftar_tugas, NAMA_BERKAS)
elif pilihan == "5":
print("Sampai jumpa!")
break
else:
print("Pilihan tidak dikenal.")
if __name__ == "__main__":
main()
storage.py
Unduh berkas"""Modul penyimpanan daftar tugas ke berkas JSON."""
import json
import os
def simpan_tugas(daftar_tugas, nama_file):
with open(nama_file, "w", encoding="utf-8") as berkas:
json.dump(daftar_tugas, berkas, ensure_ascii=False, indent=2)
def muat_tugas(nama_file):
try:
with open(nama_file, "r", encoding="utf-8") as berkas:
data = json.load(berkas)
except FileNotFoundError:
return []
except json.JSONDecodeError:
print(f"Berkas {nama_file} tidak berisi JSON yang valid.")
os.replace(nama_file, nama_file + ".bak")
print(f"Berkas yang rusak dipindah ke {nama_file}.bak.")
print("Program memulai dengan daftar tugas kosong.")
return []
if not isinstance(data, list):
print(f"Isi {nama_file} tidak berupa daftar tugas.")
return []
daftar_sah = []
for item in data:
if not isinstance(item, dict):
continue
if not isinstance(item.get("id"), int) or isinstance(item.get("id"), bool):
continue
if not isinstance(item.get("judul"), str):
continue
if not isinstance(item.get("selesai"), bool):
continue
daftar_sah.append(item)
return daftar_sah