Program kita kini lengkap secara fitur, tetapi seluruh daftar tugas lenyap begitu program berakhir — daftar_tugas hanyalah data di memori. Pada bagian ini kita memberi program ingatan permanen: setiap perubahan ditulis ke berkas tugas.json, dan setiap kali program dijalankan ulang, isi berkas tersebut dimuat kembali. Sekaligus kita memisahkan seluruh urusan penyimpanan ke module tersendiri agar main.py tetap fokus pada interaksi pengguna.

Module storage.py #

Semua kode yang membaca dan menulis berkas kita pindahkan ke storage.py:

python
"""Modul penyimpanan daftar tugas ke berkas JSON."""

import json


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:
            return json.load(berkas)
    except FileNotFoundError:
        return []

Pemisahan ini disebut separation of concerns: main.py menangani menu dan percakapan dengan pengguna, sedangkan storage.py mengetahui detail format JSON dan nama berkas. Bila kelak format penyimpanan berganti — misalnya ke SQLite — hanya storage.py yang perlu ditulis ulang. Materi Membuat Module Sendiri menjelaskan bahwa berkas Python apa pun adalah module; cukup diletakkan di folder yang sama dengan main.py agar pernyataan import storage berhasil. Kedua berkas tahap ini — main.py dan storage.py — juga tersedia pada bagian Berkas Sumber di bawah halaman.

Mari kita bedah kedua fungsinya:

  1. with open(nama_file, "w", encoding="utf-8") as berkas:: mode "w" membuka berkas untuk ditulis dan menghapus isi lamanya; context manager with menutup berkas otomatis meski terjadi exception di tengah proses. Parameter encoding="utf-8" memastikan judul berbahasa Indonesia tersimpan tanpa bergantung pada pengaturan locale mesin.
  2. json.dump(daftar_tugas, berkas, ...): fungsi dump() dari module json mengubah list of dictionary menjadi teks JSON lalu menuliskannya langsung ke file object. Argumen ensure_ascii=False menjaga huruf tetap tersimpan sebagai UTF-8 yang terbaca manusia, sedangkan indent=2 merapikan keluaran dengan indentasi dua spasi per tingkat.
  3. json.load(berkas) pada muat_tugas melakukan kebalikannya: membaca seluruh isi berkas dan mengubah teks JSON kembali menjadi list of dictionary — bentuk data yang persis sama seperti sebelum disimpan.
  4. except FileNotFoundError: return []: pada eksekusi pertama, tugas.json belum pernah dibuat, sehingga open() membangkitkan FileNotFoundError. Kondisi ini bukan galat yang perlu menghentikan program — berkas hilang berarti penyimpanan memang kosong, jadi fungsi mengembalikan list kosong.

Program Utama yang Tersambung #

main.py kini memanggil module tersebut. Berikut isi lengkapnya setelah perubahan Bagian 3 — seluruh fungsi operasi dari Bagian 2 tidak berubah, jadi perhatian kita tertuju pada tiga titik sambungan baru:

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)

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

Tiga sambungan baru inilah pekerjaan Bagian 3:

  1. import storage dan NAMA_BERKAS = "tugas.json": nama berkas penyimpanan disimpan pada satu konstanta agar mudah diganti tanpa menyentuh logika. Seluruh fungsi operasi dari Bagian 2 tetap berada di berkas yang sama tanpa perubahan.
  2. daftar_tugas = storage.muat_tugas(NAMA_BERKAS): pemuatan terjadi tepat sekali di awal main(). Sejak baris ini, daftar_tugas berisi data dari eksekusi sebelumnya — atau list kosong pada eksekusi pertama.
  3. storage.simpan_tugas(daftar_tugas, NAMA_BERKAS) setelah setiap mutasi: penyimpanan dipanggil hanya ketika data benar-benar berubah. Nilai kembalian True dari tandai_selesai() dan hapus_tugas() menjadi gerbangnya — inilah alasan fungsi operasi pada Bagian 2 sengaja melaporkan status keberhasilannya.

Sesi Uji: Data yang Bertahan #

Kita mulai dari folder kosong tanpa tugas.json, menambah dua tugas, menandai yang kedua selesai, lalu keluar:

bash
echo "1
Belajar Python dasar
1
Beli susu
3
2
5" | python3 main.py

Output yang Dihasilkan:

text
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: Judul tugas: Tugas "Belajar Python dasar" tersimpan dengan id 1.
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: Judul tugas: Tugas "Beli susu" tersimpan dengan id 2.
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: Nomor tugas: Tugas 2 ditandai selesai.
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: Sampai jumpa!

Perhatikan: tidak ada pesan galat meski tugas.json belum ada — cabang FileNotFoundError bekerja. Isi berkas yang tercipta dapat diperiksa dengan cat:

bash
cat tugas.json

Output yang Dihasilkan:

text
[
  {
    "id": 1,
    "judul": "Belajar Python dasar",
    "selesai": false
  },
  {
    "id": 2,
    "judul": "Beli susu",
    "selesai": true
  }
]

Struktur di layar persis seperti bentuk data di memori: satu list berisi dictionary dengan kunci id, judul, dan selesai. Nilai True pada Python menjadi true pada JSON, dan sebaliknya json.load() mengubahnya kembali menjadi True. Sekarang jalankan program untuk kedua kalinya:

bash
echo "2
5" | python3 main.py

Output yang Dihasilkan:

text
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: [ ] 1. Belajar Python dasar
[x] 2. Beli susu
=== Pengelola Daftar Tugas ===
1. Tambah tugas
2. Daftar tugas
3. Tandai selesai
4. Hapus tugas
5. Keluar
Pilihan: Sampai jumpa!

Kedua tugas kembali tampil — lengkap dengan penanda [x] pada Beli susu yang ditandai pada eksekusi sebelumnya. Data kini bertahan antar eksekusi.

Penyimpanan permanen telah berdiri: setiap perubahan langsung ditulis, dan setiap eksekusi memuat data terakhir. Yang tersisa adalah membuat proses pemuatan itu tahan terhadap berkas yang hilang, rusak, atau berisi data yang tidak sah — pekerjaan Bagian 4.

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)

    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


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:
            return json.load(berkas)
    except FileNotFoundError:
        return []