11 Oktober 2026•baca 4 menit

Jawabannya: Nilai Default Itu Cuma Dibuat Sekali

Tiga gejala yang kelihatan seperti satu penyakit, padahal tiga lapis beda dengan obat masing-masing. Ini urutan perbaikannya.

Repo lab

Lab-nya ada di folder 0004.mutation-and-scope

Kode yang sengaja dibuat salah, dataset-nya, dan target make check-nya ada di repo publik. Semuanya jalan di dalam Docker.

Yang menarik dari lab ini bukan satu jawabannya, tapi kenapa ada tiga. Tiga gejala — nota yang isinya sama, total Rp0, kas Rp0 — kelihatan seperti satu penyakit. Padahal tiga lapisnya beda, dan tiap lapis punya obat yang beda.

Temanya cuma satu, dan ini yang paling layak dibawa pulang: mana yang kamu ubah, dan benda apa itu sebenarnya. Tiap lapis rusak karena penulisnya mengira dia mengubah satu benda, padahal yang dia ubah benda lain — atau nggak mengubah apa pun.

Poin Penting

  • Tiga lapis beda, satu tema: mana yang kamu ubah, dan benda apa itu sebenarnya.
  • Nilai default sebuah fungsi dihitung sekali, jadi list di nilai default-nya dipakai bersama selamanya.
  • Mengubah nama lokal nggak mengubah isi dict yang dipegang pemanggil.
  • Nama yang diubah di dalam fungsi dianggap lokal, kecuali kamu bilang terus terang bahwa itu milik modul.
  • Memperbaiki lapis 1 saja masih menyisakan 4 dari 6 merah.

Hasil pengukuran

Pemeriksaannya enam. Angka di kolom kanan: berapa yang masih merah.

varian merah
kondisi awal 6 dari 6
cuma memperbaiki lapis 1 4 dari 6
ketiga lapis sekaligus 0 dari 6 (hijau)

Baris tengah itu yang paling sering kelewat. Memperbaiki lapis yang paling terkenal saja masih menyisakan 4 dari 6 merah, sebab dua masalah lain ada di tempat yang beda.

Lapis 1 — nilai default yang dihitung sekali

Ini lapis yang paling dikenal orang.

PYTHON
def pesanan_baru(meja, item=[]):   # salah

Kelihatannya wajar: "kalau pemanggil nggak mengirim item, bikin list kosong baru." Yang nggak kelihatan: nilai default sebuah fungsi dihitung sekali, waktu fungsinya baru didefinisikan di memori, bukan tiap kali dipanggil. Jadi [] di situ bukan list kosong baru setiap panggilan — dia satu list yang sama, dipakai ulang selamanya. Semua nota berbagi list itu.

PYTHON
def pesanan_baru(meja, item=None):   # benar
    if item is None:
        item = []
    return {"meja": meja, "item": item, "total": 0}

Kamu nggak perlu percaya kata orang; cek sendiri:

PYTHON
kasir.pesanan_baru(1)["item"] is kasir.pesanan_baru(2)["item"]   # harus False

Bahkan buktinya nggak perlu ditakar: pesanan_baru.__defaults__ menyimpan list itu secara harfiah. Isi satu nota, lalu lihat __defaults__ — item yang baru kamu masukkan muncul di situ juga, karena pesanan["item"] dan nilai default itu benda yang sama. Perhatikan juga: pesanan_baru sendiri nggak pernah menambah apa pun ke list itu. Yang menambah adalah fungsi lain, dan tetap saja list itu bertambah — karena cuma ada satu.

Lapis 2 — mengubah nama lokal, bukan nilai di dalam dict

PYTHON
total = pesanan["total"]        # menyalin NILAI
total = total + harga * qty     # cuma nama lokal yang berubah
return pesanan                  # nota nggak pernah tahu

total = pesanan["total"] bikin nama baru total yang menunjuk ke angka yang sama. Baris berikutnya mengubah angka di nama itu — tapi pesanan masih memegang angka lama. Nama lokal mati begitu fungsinya selesai.

PYTHON
pesanan["total"] = pesanan["total"] + harga * qty   # benar

Bedanya halus, tapi ini intinya: yang diubah adalah isi dict yang dipegang pemanggil. Mengubah nama variabel belum tentu mengubah benda yang kamu maksud — nama yang sama di dua tempat bukan jaminan benda yang sama.

Lapis 3 — nama milik modul, atau nama milik fungsi

PYTHON
TOTAL_KAS = 0

def catat_penjualan(jumlah):
    total = TOTAL_KAS        # bikin nama lokal baru
    total = total + jumlah   # cuma lokalnya yang berubah
    return total             # TOTAL_KAS nggak pernah tersentuh

Penulis aslinya berhenti di tengah jalan: niatnya menambah ke TOTAL_KAS, tapi menulisnya lewat variabel lain. Akibatnya TOTAL_KAS tetap nol sampai kapan pun. Kalau dia menulisnya langsung tanpa bilang ke Python bahwa nama itu milik modul:

PYTHON
def catat_penjualan(jumlah):
    TOTAL_KAS = TOTAL_KAS + jumlah   # UnboundLocalError

Python malah melempar UnboundLocalError. Sebabnya: begitu ada penugasan ke sebuah nama di dalam fungsi, Python menganggap nama itu lokal untuk seluruh fungsi tersebut. Waktu dia membaca TOTAL_KAS di sebelah kanan, Python belum menganggapnya ada. Ironisnya error itu justru lebih baik, karena keliatan. Penulis aslinya menghindarinya, dan itu yang bikin bugnya senyap.

PYTHON
def catat_penjualan(jumlah):
    global TOTAL_KAS                 # benar
    TOTAL_KAS = TOTAL_KAS + jumlah
    return TOTAL_KAS

global bilang ke Python: nama TOTAL_KAS di sini menunjuk yang di tingkat modul, bukan bikin yang baru di sini.

Jebakan yang gampang kejeblos

item or [] sebagai ganti if item is None. Kelihatan lebih ringkas dan di demo ini tetap benar — tapi salah begitu ada pemanggil yang sengaja mengirim list kosong sebagai argumen, karena item or [] menggantinya dengan list baru, padahal pemanggil mau mengisi list yang dia kirim.

Nambah .copy() di mana-mana biar nggak ada yang saling nimpa. .copy() bikin wadah baru, bukan isi baru. Kalau isinya benda yang bisa berubah juga, salinannya masih berbagi isi dengan aslinya. Sebelum menambah salinan, tanya dulu: yang mau kamu pisahkan wadahnya atau isinya?

Ganti nama variabel biar "beda". Kalau total diganti jadi total_nota, bugnya nggak hilang — cuma pindah nama. Yang salah bukan namanya, tapi fakta bahwa yang berubah adalah nama lokal dan bukan nilai di dalam dict.

Setelah ketiganya beres

BASH
make check    # harus 6 dari 6 LULUS
make demo     # tiga meja, tiga nota, sesuai pesanannya

Di situ make demo berhenti mengeluh, dan tiga notanya jadi tiga nota yang beda: meja 1 kopi x2 dengan total Rp30.000, meja 3 teh x1 Rp8.000, meja 5 roti x1 Rp12.000, kas harian Rp50.000.

Dan pertanyaan yang paling berguna buat dibawa keluar lab ini bukan "di mana bugnya", tapi yang satu ini: waktu kamu menulis x = ..., benda mana yang sebenarnya kamu ubah? Jawab itu dengan benar, dan ketiga lapis di atas berhenti jadi misteri.

Pertanyaan buat kamu

  1. Kalau pesanan_baru menerima list dari pemanggilnya, siapa yang seharusnya bertanggung jawab waktu list itu berubah setelahnya — yang mengirim, atau yang menerima?
  2. Lapis 2 dan lapis 3 kelihatan seperti dua bug yang beda. Tapi keduanya bisa diringkas jadi satu pertanyaan yang sama persis. Pertanyaan apa itu?
  3. Kalau TOTAL_KAS bukan angka melainkan dict, misalnya {"kas": 0}, apakah lapis 3 masih perlu bilang ke Python bahwa nama itu milik modul? Coba di kertas dulu, baru di terminal.

Biar makin jago ngoding.

Yuk gabung bareng temen-temen developer lainnya buat dapet update teknologi, tips, dan tutorial santai tiap minggu.

Biar Nggak Kudet

Update teknologi, tips ngoding, dan insight santai langsung ke email kamu tiap minggu.

Santai, anti spam. Bisa berhenti langganan kapan aja.

Baca Selanjutnya

Lihat semua

© 2026 Inva.dev.