Kenapa Mutable Default Argument di Python Bisa Jadi Bug?

1. ‘Sepele Tapi Menjebak’: Apa Itu Default Argument di Python?

 Kalau kamu sudah pernah menulis fungsi di Python, pasti pernah melihat atau bahkan menggunakan default argument. Ini adalah fitur yang membuat parameter pada fungsi bisa punya nilai bawaan. Misalnya, kamu menulis:

 def sapa(nama=”Dunia”):     print(f”Halo, {nama}!”)

 Kalau fungsi sapa() dipanggil tanpa argumen, otomatis akan mencetak Halo, Dunia!. Simpel, kan? Tapi di balik kesederhanaan ini, ada jebakan yang sering bikin pusing, terutama kalau kamu pakai tipe data mutable seperti list atau dict sebagai default argument.

Mutable vs Immutable: Kenapa Penting?

 Di Python, tipe data terbagi dua: mutable (bisa diubah, seperti list, dict, set) dan immutable (tidak bisa diubah, seperti int, str, tuple). Nah, masalah muncul saat kamu pakai mutable object sebagai default argument. Penelitian dan dokumentasi Python menunjukkan bahwa Python mengevaluasi default argument hanya sekali, saat fungsi didefinisikan, bukan setiap kali fungsi dipanggil. Ini beda dengan ekspektasi banyak programmer yang terbiasa dengan bahasa lain.

Contoh Bug yang Sering Terjadi

 def tambah_item(item, daftar=[]):     daftar.append(item)     return daftar  print(tambah_item(‘apel’))    # [‘apel’] print(tambah_item(‘jeruk’))   # [‘apel’, ‘jeruk’]  # Loh, kok jadi nambah terus?

 Di sini, daftar ternyata menyimpan barang lama dari pemanggilan sebelumnya. Analogi sederhananya: bayangkan kamu punya lemari penyimpanan. Setiap kali kamu masukin barang, barang sebelumnya nggak pernah benar-benar diambil keluar. Akhirnya, lemari itu penuh dengan barang-barang lama yang nggak kamu sadari masih ada di sana.

Trivia Sejarah: Kenapa Bisa Begini?

 Sedikit trivia, perilaku ini adalah hasil desain Python sejak awal. Guido van Rossum, pencipta Python, memilih agar default argument dievaluasi sekali saja demi efisiensi. Tapi, efek sampingnya, bug seperti di atas sering bikin bingung pemula. Banyak sumber menyebut ini sebagai salah satu “gotcha” klasik Python.

Pengalaman Pribadi: Fungsi yang ‘Mengisi Sendiri’

 Saya sendiri pernah terjebak. Waktu itu, saya bikin fungsi untuk menambah tugas ke daftar. Tiba-tiba, daftar tugas jadi panjang sendiri, padahal saya yakin sudah reset tiap kali. Ternyata, fungsi saya ‘mengisi sendiri’ dari masa lalu karena pakai list sebagai default argument.

Kapan Default Argument Tetap Berguna?

 Walau berbahaya kalau salah pakai, default argument tetap powerful. Untuk tipe immutable seperti None, int, atau str, kamu bisa pakai dengan aman. Tapi, untuk tipe mutable, best practice-nya: gunakan None sebagai default, lalu inisialisasi di dalam fungsi. Studi kasus di banyak proyek Python nyata membuktikan, cara ini jauh lebih aman dan mudah di-debug.

2. Mutable vs Immutable: Kunci Memahami Sumber Masalah

 Sebelum kamu bisa menghindari jebakan mutable default argument di Python, kamu harus paham dulu perbedaan antara mutable dan immutable. Ini bukan sekadar istilah teknis—ini kunci utama memahami kenapa bug aneh bisa muncul tanpa kamu sadari.

Bedakan Mutable dan Immutable

 Di Python, mutable artinya objek bisa diubah setelah dibuat. Contohnya: list, dict, dan set. Kamu bisa menambah, menghapus, atau mengedit isinya kapan saja. Sedangkan immutable seperti tuple, int, dan str—sekali dibuat, isinya tidak bisa diubah. 

 Analogi sederhananya: mutable itu seperti plastisin—bisa dibentuk ulang kapan saja. Immutable seperti batu—sekali dibentuk, tidak bisa diubah tanpa menghancurkannya.

Studi Kecil: List Default Argument yang ‘Seram’

 Coba lihat kode berikut:

 def tambah_item(item, daftar=[]):   daftar.append(item)   return daftar

 Kelihatannya aman, ya? Tapi coba panggil beberapa kali:

 print(tambah_item(1))  # [1] print(tambah_item(2))  # [1, 2] print(tambah_item(3))  # [1, 2, 3]

 Lho, kenapa daftar terus bertambah? Research shows Python mengevaluasi default argument hanya sekali saat fungsi didefinisikan, bukan setiap kali dipanggil. Jadi, daftar yang dipakai selalu objek yang sama!

Mengapa Tuple Nyaris Selalu Aman?

 Karena tuple itu immutable, kamu tidak bisa mengubah isinya setelah dibuat. Jadi, kalau kamu pakai tuple sebagai default argument, tidak ada risiko data berubah tanpa sengaja. Tips: kalau butuh default argument yang tidak berubah, pilih tipe data immutable.

FAQ Singkat: Pernah Menyesal Pakai Dict Kosong?

 Jujur saja, siapa yang belum pernah menulis seperti ini?

 def update_config(config={}):   config[‘foo’] = ‘bar’   return config

 Saya pernah. Dan hasilnya? Semua pemanggilan fungsi berikutnya mewarisi perubahan dari pemanggilan sebelumnya. Seringkali, bug ini baru ketahuan saat aplikasi sudah berjalan lama atau saat debugging manual.

Dampak Nyata di Debugging

 Bug akibat mutable default argument sering bersembunyi di balik testing manual yang tidak teliti. Kadang, error baru muncul setelah fungsi dipanggil berkali-kali. Studies indicate ini salah satu “gotcha” klasik di Python yang bikin pusing saat debugging.

 Solusi paling aman? Gunakan None sebagai default, lalu inisialisasi di dalam fungsi:

 def tambah_item(item, daftar=None):   if daftar is None:     daftar = []   daftar.append(item)   return daftar

 Dengan cara ini, setiap pemanggilan fungsi selalu mendapat objek baru—tidak ada lagi plastisin yang menempel di mana-mana!

3. Waktunya ‘Ngoding Horror’: Contoh Kode Berujung Bug yang Bikin Frustasi

 Pernahkah kamu merasa heran kenapa hasil fungsi Python yang kamu panggil berkali-kali tiba-tiba berubah sendiri? Atau, list yang seharusnya kosong tiba-tiba penuh dengan data dari pemanggilan sebelumnya? Inilah salah satu “ngoding horror” yang sering bikin frustrasi, terutama kalau kamu baru belajar Python atau belum paham soal mutable default argument.

Contoh Kode Nyata: Fungsi dengan List sebagai Default Argument

 Coba perhatikan kode berikut:

 def tambah_item(item, daftar=[]):   daftar.append(item)   return daftar

 Sekilas, kode ini terlihat sederhana dan aman. Tapi, coba jalankan beberapa kali:

 print(tambah_item(‘apel’))   # Output: [‘apel’] print(tambah_item(‘jeruk’))  # Output: [‘apel’, ‘jeruk’] print(tambah_item(‘pisang’)) # Output: [‘apel’, ‘jeruk’, ‘pisang’]

 Kok bisa? Kenapa list-nya makin panjang setiap fungsi dipanggil, padahal kamu ingin setiap pemanggilan punya list baru? Inilah efek “tersembunyi” dari mutable default argument.

Membongkar: Tracing Perilaku Objek yang Tersembunyi

 Menurut research, Python mengevaluasi default argument hanya sekali saat fungsi didefinisikan, bukan setiap kali fungsi dipanggil. Jadi, list daftar di atas adalah objek yang sama untuk semua pemanggilan fungsi. Kalau kamu mengubahnya di satu tempat, perubahan itu akan “nempel” di pemanggilan berikutnya.
 Seperti yang sering disebutkan dalam dokumentasi Python:

“Default argument values are evaluated only once, at the point of function definition in the defining scope.”

Studi Singkat: Kejutan Bug dalam Proyek Scraper

 Dalam sebuah proyek scraper, saya pernah membuat kelas seperti ini:

 class Scraper:   def __init__(self, tasks=set()):     self.tasks = tasks

 Ternyata, setiap instance Scraper yang saya buat, self.tasks-nya saling berbagi data! Hasilnya, data dari satu scraper bisa “bocor” ke scraper lain. Ini jelas bikin debugging makin ribet dan bikin stres.

Apa yang Berbeda Jika Default-nya None atau Tuple?

 Solusi yang direkomendasikan komunitas Python adalah menggunakan None sebagai default, lalu inisialisasi di dalam fungsi:

 def tambah_item(item, daftar=None):   if daftar is None:     daftar = []   daftar.append(item)   return daftar

 Dengan cara ini, setiap pemanggilan fungsi akan mendapatkan list baru. Kalau kamu pakai tipe immutable seperti tuple, masalah ini tidak akan terjadi karena tuple tidak bisa diubah setelah dibuat.

Ayo Diskusi: ‘Kebiasaan Buruk’ yang Masih Sering Dipraktikkan Pemula Python

 Banyak pemula (dan bahkan yang sudah berpengalaman) masih sering terjebak menggunakan mutable default argument tanpa sadar. Padahal, ini salah satu “gotcha” klasik Python yang sering dibahas di forum dan dokumentasi resmi.
 Sudah saatnya kita lebih hati-hati dan membiasakan diri menulis kode yang aman dari bug tersembunyi seperti ini.

4. ‘What Actually Happens’: Di Balik Layar Evaluasi Default Argument

Pernahkah kamu bertanya-tanya kenapa bug aneh bisa muncul saat menggunakan default argument bertipe mutable di Python? Untuk benar-benar memahami masalah ini, kamu perlu tahu apa yang sebenarnya terjadi di balik layar ketika Python menangani default argument.

Sekilas: Bagaimana Python Menyimpan Default Argument

Ketika kamu mendefinisikan sebuah fungsi dengan default argument, misalnya def tambah_item(item, daftar=[]):, Python tidak membuat daftar baru setiap kali fungsi dipanggil. Sebaliknya, Python langsung mengevaluasi nilai default itu sekali saja—tepat saat fungsi didefinisikan, bukan saat dipanggil. Hasilnya, objek daftar yang sama akan digunakan berulang-ulang setiap kali fungsi itu dipanggil tanpa argumen daftar baru.

Visualisasi: Lemari Global Tempat Barang Disimpan Bersama

Coba bayangkan kamu punya satu lemari global di rumah. Setiap kali ada tamu datang tanpa membawa tas sendiri, kamu pinjamkan tas yang sama dari lemari itu. Kalau satu tamu meninggalkan barang di tas, tamu berikutnya akan menemukan barang itu juga. Begitulah cara Python memperlakukan objek default yang mutable—semua “tamu” (pemanggilan fungsi) berbagi satu tas (objek list/dict/set yang sama).

Teknis: Evaluasi Saat Definisi vs Pemanggilan

Secara teknis, default argument di Python dievaluasi sekali saja, saat fungsi didefinisikan. Ini berbeda dengan bahasa lain seperti JavaScript, di mana default parameter dievaluasi setiap kali fungsi dipanggil. Di Python, jika kamu mengubah isi objek default itu (misal, menambah elemen ke list), perubahan itu akan “menempel” di pemanggilan berikutnya.

Mendeteksi Pola Bug Ini Saat Debugging

Bagaimana kamu bisa tahu kalau bug ini sedang terjadi? Biasanya, kamu akan melihat perilaku aneh seperti data yang “bocor” antar pemanggilan fungsi. Misalnya, kamu memanggil fungsi dua kali tanpa argumen list, dan tiba-tiba list yang kedua sudah berisi data dari pemanggilan pertama. Jika kamu menemukan pola seperti ini, besar kemungkinan kamu terjebak di “lemari global” Python.

Anekdot: Modul yang Harus Dihapus dan Ditulis Ulang

Seorang teman pernah bercerita, ia harus menghapus dan menulis ulang satu modul Python gara-gara bug ini. Awalnya, ia tidak sadar list default yang ia pakai di fungsi ternyata “menyimpan dendam” dari pemanggilan sebelumnya. Setelah berjam-jam debugging dan membaca dokumentasi, akhirnya ia sadar: “Ternyata list itu bukan baru setiap kali, tapi satu list yang sama terus-menerus!”

Perbandingan Singkat dengan Bahasa Lain

Kalau kamu pernah pakai JavaScript atau C#, kamu mungkin tidak menemukan masalah ini. Di JavaScript, default parameter dievaluasi setiap kali fungsi dipanggil, jadi tidak ada “lemari global”. Di C#, default parameter harus berupa nilai konstan, sehingga tidak bisa berupa objek mutable seperti list atau dictionary. Jadi, masalah ini cukup unik di Python dan sering jadi “gotcha” bagi pemula maupun yang sudah berpengalaman.

5. Jalan Pintas Menuju Best Practice: Cara Menulis Default Argument dengan Aman

 Kamu pasti pernah mendengar saran klasik dalam komunitas Python: “Jangan pernah pakai objek mutable sebagai default argument!” Tapi kenapa sih, dan apa jalan pintasnya supaya kode kamu tetap aman dari bug tersembunyi ini?

Rumus Utama: Gunakan None Sebagai Default

 Praktik terbaik yang sudah jadi “resep wajib” di dokumentasi resmi Python adalah: gunakan None sebagai default argument untuk parameter yang ingin kamu isi dengan objek mutable seperti list atau dict. Setelah itu, lakukan pengecekan di dalam fungsi:

def tambah_item(item, daftar=None):     if daftar is None:         daftar = []     daftar.append(item)     return daftar

 Dengan pola ini, setiap kali fungsi dipanggil tanpa argumen, Python akan membuat list baru. Tidak ada lagi efek samping aneh seperti item lama yang tiba-tiba muncul di pemanggilan berikutnya.

Kenapa Praktik Ini Jadi Idiomatik?

 Research shows, pola if arg is None: arg = [] ini sangat populer dan bahkan disebutkan di banyak buku dan dokumentasi Python. Alasannya sederhana: Python mengevaluasi default argument hanya sekali saat fungsi didefinisikan, bukan setiap kali fungsi dipanggil. Kalau kamu pakai list kosong langsung ([]), list itu akan terus dipakai ulang di setiap pemanggilan fungsi. Ini sering jadi “gotcha” yang bikin pusing, terutama buat pemula.

Alternatif Cerdas: Inisialisasi dengan copy() di Class

 Saat kamu menulis class, kadang kamu ingin menerima list dari luar tapi tetap ingin punya salinan sendiri. Trik yang sering dipakai adalah:

class TaskManager:     def __init__(self, tasks=None):         self.tasks = list(tasks) if tasks is not None else []

 Dengan cara ini, setiap instance punya list sendiri, meskipun argumen awalnya sama.

Tips Pro: Review Kode Lama

 Saran penting: cek ulang kode lama kamu. Banyak bug lawas yang ternyata bersumber dari penggunaan mutable default argument. Kadang bug ini baru muncul setelah aplikasi berjalan lama atau saat ada perubahan kecil di fungsi lain.

Refleksi: Solusi Sederhana, Efek Besar

 Solusi None ini memang kelihatan sepele, tapi bisa menyelamatkan kamu dari jam kerja sia-sia untuk debugging. Studi kasus di berbagai proyek Python membuktikan, bug dari mutable default argument seringkali sulit dilacak dan bikin frustrasi.

Perdebatan: Kapan Mutable Default Argument Boleh Dipakai?

 Ada kalanya, walau sangat jarang, kamu memang ingin efek “shared state” antar pemanggilan fungsi. Namun, ini harus dilakukan dengan sadar dan biasanya untuk kasus khusus seperti cache sederhana. Tetap, best practice-nya: hindari kecuali benar-benar paham risikonya.

6. Studi Kasus: Ketika Mutable Default Menghantui Proyek Python Sungguhan

Pernahkah kamu mengalami bug misterius yang muncul tiba-tiba di project Python, padahal kode sudah berjalan mulus selama berminggu-minggu? Salah satu penyebab yang sering tidak disadari adalah penggunaan mutable default argument. Studi kasus berikut ini diambil dari pengalaman nyata dalam sebuah proyek web scraping yang cukup besar.

Kisah Nyata: Data ‘Tembus’ ke Session Lain

Di sebuah modul shared helper, ada fungsi yang bertugas mencatat history aktivitas user selama scraping. Fungsi ini tampak sederhana, misalnya:

 def add_history(user_id, history=[]):     history.append(f”User {user_id} melakukan scraping”)     return history

Awalnya, semua berjalan baik. Tapi, setelah beberapa waktu, tim menemukan kasus aneh: riwayat user A tiba-tiba muncul di session user B. Data ‘tembus’ antar user, padahal seharusnya setiap user punya history sendiri.

Proses Investigasi: Kenapa Bisa Bocor?

Tim mulai menelusuri kode. Ternyata, masalahnya ada pada default argument berupa list kosong ([]). Research shows, di Python, default argument dievaluasi satu kali saat fungsi didefinisikan, bukan setiap kali fungsi dipanggil. Artinya, list history pada contoh di atas akan terus digunakan ulang di setiap pemanggilan fungsi, sehingga data dari satu user bisa ‘bocor’ ke user lain.

Langkah Perbaikan: Refactor dengan None

Solusinya sederhana tapi sangat ampuh. Ubah default argument menjadi None, lalu inisialisasi list baru di dalam fungsi:

 def add_history(user_id, history=None):     if history is None:         history = []     history.append(f”User {user_id} melakukan scraping”)     return history

Setelah refactor, bug langsung hilang. Setiap user kini mendapatkan history yang benar-benar terpisah.

Efek Nyata: Debugging Lebih Cepat, Tim Lebih Bahagia

Perbaikan ini membuat proses debugging jadi jauh lebih cepat. Tidak ada lagi kasus aneh yang bikin pusing. Tim pun bisa deploy fitur baru tanpa rasa was-was. Studi dan dokumentasi Python memang menekankan, “Mutable default arguments are considered an anti-pattern and a common source of bugs in Python code.”

Lessons Learned: Pentingnya Peer-Review & Kode Lama

Kasus ini jadi pengingat betapa pentingnya peer-review dan membaca ulang kode lama. Kadang, bug seperti ini lolos karena dianggap sepele. Padahal, efeknya bisa sangat luas.

Wild Card: Bagaimana Jika Python Mengubah Cara Evaluasi?

Pernahkah kamu membayangkan, bagaimana jika Python mengubah cara evaluasi default argument menjadi dieksekusi setiap kali fungsi dipanggil? Dampaknya bakal besar ke backward compatibility dan ribuan project open source. Banyak kode lama bisa rusak, dan dokumentasi harus diperbarui. Jadi, meski kadang membingungkan, keputusan Python ini punya alasan historis dan teknis yang kuat.

7. Kesimpulan: Kuasai Python, Jangan Terjebak Bug Klasik Mutable Default Argument!

 Setelah membongkar rahasia di balik mutable default argument di Python, kamu pasti sadar betapa mudahnya bug ini muncul—bahkan di tangan programmer yang sudah berpengalaman sekalipun. Sumber masalahnya sederhana: Python mengevaluasi nilai default argument hanya sekali, saat fungsi didefinisikan, bukan setiap kali fungsi dipanggil. Akibatnya, jika kamu menggunakan tipe data mutable seperti list atau dict sebagai default argument, perubahan pada objek itu akan menempel di setiap pemanggilan berikutnya. Ini seringkali jadi jebakan yang tidak disadari.

 Coba bayangkan, kamu menulis fungsi seperti ini:

 def tambah_item(item, daftar=[]):   daftar.append(item)   return daftar

 Awalnya, kelihatan aman-aman saja. Tapi begitu kamu panggil beberapa kali, hasilnya bisa bikin kaget. Daftar yang kamu kira “kosong” di awal pemanggilan ternyata sudah berisi sisa item dari pemanggilan sebelumnya. Analogi sederhananya, mutable default argument itu seperti “tas bersama”—semua orang bisa masukin barang, dan isinya bakal campur aduk kalau tidak hati-hati!

 Solusi yang direkomendasikan komunitas Python sangat jelas: gunakan None sebagai default, lalu inisialisasi objek mutable di dalam fungsi. Contohnya:

 def tambah_item(item, daftar=None):   if daftar is None:     daftar = []   daftar.append(item)   return daftar

 Dengan pola ini, setiap pemanggilan fungsi akan mendapatkan objek list baru, sehingga tidak ada “tas bersama” yang bikin isi data jadi campur aduk. Praktik ini sudah menjadi best practice di dunia Python, dan sering disebut dalam berbagai sumber seperti Python Guide Gotchas, Python Morsels, maupun diskusi komunitas.

 Menariknya, bug ini sering kali tidak langsung kelihatan. Banyak kasus di mana fungsi lawas yang sudah bertahun-tahun berjalan tiba-tiba menimbulkan error aneh, hanya karena ada mutable default argument yang terlewat. Jadi, tantangan buat kamu: coba review kode lama besok, siapa tahu ada ‘harta karun’ bug yang tersembunyi!

 Terakhir, mari berdiskusi: pernahkah kamu atau temanmu terjebak bug klasik ini? Ceritakan pengalamanmu di kolom komentar. Untuk memperdalam pemahaman, kamu bisa cek referensi lanjutan seperti Python Guide Gotchas, Python Morsels, atau pengalaman komunitas yang sering membahas topik ini. Ingat, memahami perbedaan mutable dan immutable bukan cuma soal teori—ini kunci menulis kode Python yang aman dan andal.