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:

KondisiContoh isi berkasPenanganan
Berkas hilangtugas.json belum pernah dibuatmulai dengan list kosong
JSON rusakteks terpotong, tanda kutip hilangpindahkan berkas sebagai cadangan, mulai kosong
Record tidak sahid berupa string, kunci hilanglewati 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:

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

  1. os.replace(nama_file, nama_file + ".bak"): fungsi os.replace() dari module os memindahkan berkas lama ke nama baru, menimpa .bak sebelumnya 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.
  2. return [], bukan raise: 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:

python
    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

Mari kita bedah aturan validasinya:

  1. 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 loop for gagal karena mencoba mengiterasi sesuatu yang tidak iterable.
  2. if not isinstance(item, dict): continue: method .get() hanya dimiliki dictionary. Record seperti "bukan dictionary" dilewati pada pemeriksaan pertama.
  3. Pemeriksaan per kunci dengan item.get(...): method get() mengembalikan None ketika kunci tidak ada — dan None gagal di semua uji isinstance berikutnya, sehingga record dengan kunci hilang otomatis tersaring.
  4. or isinstance(item.get("id"), bool): pengaman khusus yang terlihat janggal. Di Python, bool adalah subclass dari int, sehingga isinstance(True, int) bernilai True. Tanpa suku kedua, record dengan "id": true akan lolos sebagai id 1 dan bertabrakan dengan tugas asli bernomor 1.
  5. continue, bukan raise: 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:

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

Perubahan 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():

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

bash
echo "{ id: 1, judul: rusak tanpa tanda kutip }" > tugas.json
echo "2
5" | python3 main.py

Output yang Dihasilkan:

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

json
[
  {"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:

bash
echo "2
5" | python3 main.py

Output yang Dihasilkan:

text
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
python
"""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
python
"""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