Tutorial

Python Sentinel Value: PEP 661 dan sentinel Bawaan Python 3.15

Python Sentinel Value: PEP 661 dan sentinel Bawaan Python 3.15

None di Python memikul terlalu banyak arti: nilai kosong, parameter tidak diisi, penanda error, dan nilai sah sekaligus. Akibatnya sebuah cache mengembalikan None baik untuk key yang tidak ada maupun untuk key yang memang bernilai None, dan pemanggil tidak bisa membedakan keduanya. Solusinya sentinel value, yaitu objek penjaga di luar domain nilai yang sah sehingga tidak mungkin bertabrakan dengan data asli. Sejak Python 3.15, sentinel menjadi built-in setelah PEP 661 diterima Steering Council, sekitar sembilan hari setelah sempat ditunda di PyConDE April 2026.

TL;DR

  • None ambigu karena dipakai untuk "tidak ada", "belum diisi", "error", dan nilai sah sekaligus.
  • Sentinel adalah objek di luar domain nilai valid, jadi tidak bisa bertabrakan dengan data nyata.
  • Pola klasik MISSING = object() jalan saat runtime, tapi merusak type narrowing di static type checker.
  • PEP 661 sempat ditunda di PyConDE April 2026, lalu diterima Steering Council sekitar sembilan hari kemudian.
  • Sejak Python 3.15, sentinel jadi built-in dan bisa di-narrow oleh type checker seperti None; typing_extensions menyediakan backport.

Kenapa None jadi masalah?

Karena satu nilai dipakai untuk beberapa makna yang seharusnya dibedakan. Contoh paling sering muncul di cache: sebuah fungsi mengembalikan None ketika key tidak ditemukan, tapi juga mengembalikan None ketika key ditemukan dan nilainya memang None. Pemanggil yang menerima None tidak punya cara mengetahui apakah ia harus menghitung ulang nilai itu atau cukup memakai hasil yang tersimpan.

Pola serupa muncul di tempat lain. Pada pembaruan konfigurasi, timeout=None bisa berarti "matikan timeout" atau "jangan ubah nilai yang sekarang". Pada fungsi yang menelan TimeoutError lalu mengembalikan None, nilai itu berubah fungsi menjadi sinyal error. Ketiga kasus punya akar yang sama: satu nilai dipakai untuk mewakili keadaan yang berbeda-beda, dan pembedaannya hanya ada di kepala penulis kode, bukan di dalam tipe data.

Apa itu sentinel value sebenarnya?

Sentinel adalah objek yang dijamin berada di luar himpunan nilai sah, sehingga keberadaannya selalu bisa dibedakan dari data. Kalau domain nilai sebuah parameter adalah string, maka sebuah objek khusus yang bukan string tidak mungkin dikirim pengguna secara tidak sengaja. Itulah gunanya: ia menjadi penanda yang tidak bisa dipalsukan oleh data.

Python sendiri sudah lama memakai sentinel, hanya saja jarang disadari. Ellipsis dipakai untuk melewati dimensi pada slicing NumPy dan menandai stub. NotImplemented berarti "cari implementasinya di tempat lain", dan itulah yang mengaktifkan double dispatch pada operator biner lewat metode seperti __rsub__. Pydantic memakai MISSING untuk membedakan field yang secara eksplisit diset null dari field yang tidak dikirim sama sekali, dan statusnya baru keluar dari tahap eksperimental pada Pydantic 2.14.

PendekatanKelebihanKelemahan
None sebagai penandaSederhana, tidak perlu imporAmbigu, tidak bisa dibedakan dari nilai sah
MISSING = object()Jalan di runtime, tidak bisa bertabrakanMerusak type narrowing di static type checker
Literal string khususMudah dibaca, aman untuk serialisasiBisa bertabrakan dengan data string nyata
Enum satu anggotaPunya nama dan bisa diimporIdentitas bisa rusak saat copy atau pickle
sentinel bawaan Python 3.15Bisa di-narrow type checker seperti NoneButuh Python 3.15 atau backport, dukungan checker masih menyusul

Kenapa MISSING = object() tidak cukup?

Karena pola itu bekerja sempurna saat runtime tapi gagal di tahap analisis statis. Type checker tidak punya cara mengetahui bahwa parameter bertipe str | object sebenarnya berarti "string atau sentinel", sehingga narrowing tidak terjadi. Akibatnya setiap akses setelah pemeriksaan tetap dianggap mungkin sentinel, dan developer terpaksa menambahkan cast atau assert yang mengotori kode.

Ada beberapa akal-akalan yang biasa dipakai. Guard TypeIs bisa memberi tahu type checker bahwa setelah pemeriksaan tertentu nilainya pasti string. Literal string ajaib seperti "__missing__" mudah dibaca dan aman untuk serialisasi, tapi bisa bertabrakan dengan data string nyata. Enum satu anggota memberi nama dan bisa diimpor, tapi identitasnya bisa rusak ketika objek disalin atau di-pickle, dan pemeriksaan is langsung gagal. Semua jalan pintas itu punya biaya masing-masing.

MISSING = object()

def set_email(user_id: int, email: str | None = MISSING) -> None:
    if email is MISSING:
        return
    simpan(user_id, email.lower())

Kode di atas benar saat dijalankan, tapi type checker tetap menganggap email mungkin bertipe object di dalam blok setelah pemeriksaan. Itu sumber keluhan yang berulang: solusinya bekerja, tapi memaksa kompromi pada alat yang seharusnya membantu.

Bagaimana PEP 661 menyelesaikannya?

PEP 661 mengusulkan sentinel sebagai konsep bahasa, bukan trik library. Riwayatnya berbelit: pada PyConDE April 2026 penulisnya harus menyimpulkan bahwa proposal itu ditunda, lalu hanya sembilan hari kemudian Steering Council menerimanya. Sejak Python 3.15, sentinel menjadi built-in, dan type checker bisa me-narrow-nya persis seperti None.

from typing import sentinel

MISSING = sentinel("MISSING")

def update_email(user_id: int, email: str | None | MISSING = MISSING) -> None:
    if email is not MISSING:
        simpan_email(user_id, email.lower())

Perhatikan bedanya dengan versi sebelumnya. Anotasi tipenya sekarang menyebut sentinel secara eksplisit, sehingga setelah pemeriksaan is not MISSING, tipe email menyempit menjadi str | None tanpa cast. Inilah yang tidak bisa dicapai oleh object() biasa, dan alasan mengapa perubahan ini lebih dari sekadar kosmetik.

Apakah bisa dipakai di versi Python lama?

Bisa, lewat typing_extensions yang menyediakan backport. Yang perlu diperhatikan, dukungan type checker masih menyusul, jadi perilakunya bisa berbeda antaralat. Sebelum mengandalkan narrowing, uji dulu dengan checker yang benar-benar dipakai di proyek, karena dukungan yang belum merata berarti hasilnya bisa lolos di satu alat dan gagal di alat lain.

Untuk kode yang harus jalan di beberapa versi Python sekaligus, pola yang aman adalah mengimpor dari typing dengan fallback ke typing_extensions. Dengan begitu kode tetap kompatibel ke belakang, dan begitu versi minimum proyek naik ke 3.15, impor fallback-nya bisa dihapus tanpa mengubah logika.

Kapan sebaiknya memakai sentinel?

Pakai ketika ada parameter atau nilai balik yang perlu membedakan "tidak diberikan" dari "diberikan sebagai kosong". Ini paling sering terjadi pada fungsi pembaruan sebagian, cache, dan API yang membedakan field tidak dikirim dengan field dikirim bernilai null. Kalau tidak ada kebutuhan membedakan dua keadaan itu, sentinel hanya menambah kerumitan tanpa manfaat.

Sebaliknya, jangan pakai sentinel sebagai pengganti exception. Fungsi yang gagal karena alasan nyata sebaiknya melempar error, bukan mengembalikan penanda. Sentinel menjawab pertanyaan "apakah nilai ini diberikan", sedangkan exception menjawab "apakah operasi ini berhasil". Mencampur keduanya mengembalikan masalah yang justru ingin diselesaikan, yaitu satu nilai yang memikul terlalu banyak makna.

Bagaimana bermigrasi tanpa merusak kode yang sudah ada?

Jangan mengganti semua pemakaian sekaligus. Mulai dari satu modul dengan cakupan kecil, ganti None yang bermakna ganda menjadi sentinel eksplisit, lalu jalankan test suite. Kalau hijau, perluas ke modul berikutnya. Migrasi bertahap memberi dua manfaat: bug yang muncul bisa langsung dikaitkan dengan satu perubahan, dan kalau ada bagian yang ternyata tidak butuh sentinel, tidak ada yang perlu dibongkar.

Perhatikan juga batas antarproses. Sentinel buatan sendiri tidak bertahan melewati serialisasi, karena yang dikirim ke proses lain hanyalah representasi datanya, bukan identitas objeknya. Kalau penanda harus melewati antrean pesan, API, atau cache lintasproses, gunakan representasi yang aman seperti nilai khusus yang disepakati atau kolom terpisah yang menyatakan ada tidaknya nilai. Memaksakan sentinel object melewati batas itu hanya menghasilkan None yang kembali ambigu di sisi penerima.

Untuk API publik, pertimbangkan efeknya terhadap pemanggil. Menambahkan parameter dengan sentinel sebagai nilai bawaan aman untuk kompatibilitas, karena pemanggil lama tidak perlu diubah. Sebaliknya, mengubah arti None pada parameter yang sudah ada adalah perubahan yang memutus, karena pemanggil yang mengirim None dengan maksud lama akan mendapat perilaku berbeda tanpa peringatan.

Ada satu pertimbangan terakhir yang jarang dibahas: dokumentasi. Sentinel yang tidak dinamai dengan jelas akan terbaca sebagai nilai ajaib oleh orang berikutnya, dan nilai ajaib selalu berakhir salah tafsir. Beri nama yang menyatakan maksudnya, misalnya NOT_SET atau MISSING, bukan SENTINEL atau X. Nama yang baik menghilangkan kebutuhan untuk membaca implementasi, dan itu justru tujuan utama dari seluruh pembahasan ini.

Sumber dan bacaan lanjut

  • Teks resmi proposal sentinel di peps.python.org, termasuk riwayat penundaan dan penerimaannya.
  • Dokumentasi modul typing di docs.python.org untuk perilaku narrowing dan tipe khusus.
  • Dokumentasi Pydantic, tempat MISSING dipakai membedakan field tidak dikirim dari field bernilai null.
  • Artikel dan slide asli dari penulisnya: Florian Wilhelm.

FAQ

Apakah sentinel sudah tersedia di Python 3.14?

Tidak, sesuai penjelasan penulis proposal, sentinel menjadi built-in sejak Python 3.15. Untuk versi sebelumnya, gunakan backport dari typing_extensions. Periksa juga dokumentasi versi Python yang dipakai, karena ketersediaan fitur bisa berbeda dari yang tertulis di artikel atau slide presentasi.

Kenapa pemeriksaan identitas bisa gagal pada sentinel buatan sendiri?

Karena is membandingkan identitas objek, dan identitas bisa berubah ketika objek disalin atau di-pickle. Proses yang menerima objek hasil unpickle akan mendapat instance baru, bukan objek yang sama, sehingga is mengembalikan False. Sentinel bawaan bahasa menghindari masalah ini karena desainnya sejak awal memperhitungkan perilaku tersebut.

Apakah Pydantic MISSING bisa dipakai di luar Pydantic?

Secara teknis bisa diimpor, tapi tidak disarankan karena coupling ke library yang tidak ada hubungannya dengan modul tersebut. Saran penulis proposalnya jelas: pakai sentinel yang sudah disediakan library kalau memang bekerja di dalam library itu, dan pakai sentinel bawaan bahasa untuk kode sendiri. Mencampur keduanya membuat ketergantungan sulit dilacak.

Apakah sentinel menggantikan pola exception untuk error?

Tidak. Sentinel menjawab pertanyaan apakah sebuah nilai diberikan, bukan apakah operasi berhasil. Fungsi yang menelan TimeoutError lalu mengembalikan None bukan kasus sentinel, melainkan kasus exception yang ditangani dengan buruk. Mengganti None itu dengan sentinel hanya memindahkan ambiguitasnya, bukan menghilangkannya.

Bagaimana memilih antara sentinel dan overload fungsi?

Kalau perbedaan perilakunya besar dan jumlah kombinasinya sedikit, overload sering lebih jelas karena tiap varian punya tanda tangan sendiri. Sentinel lebih cocok ketika yang dibedakan hanya satu keadaan tambahan pada parameter yang sama. Kalau jumlah parameter yang butuh penanda lebih dari dua, itu sinyal bahwa fungsinya perlu dipecah.

💬 Komentar (0)

Belum ada komentar. Jadilah yang pertama! 💬

Komentar yang memuat tautan, bahasa asing, atau aksara non-Latin ditahan dulu buat ditinjau.