1 poin oleh GN⁺ 1 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • PEP 661 mengusulkan objek callable bawaan Python sentinel() dan C API PySentinel_New() untuk membuat nilai sentinel yang bisa dibedakan secara terpisah saat None merupakan nilai yang valid
  • Idiom lama _sentinel = object() memiliki repr yang panjang dan tidak jelas dalam signature fungsi, serta dapat menimbulkan masalah pada signature tipe yang jelas, penyalinan, dan pickling
  • Pemanggilan sentinel('MISSING') membuat objek unik baru dengan repr singkat, dan jika ingin berbagi sentinel yang sama, harus secara eksplisit digunakan ulang dengan menugaskannya ke variabel seperti MISSING = sentinel('MISSING')
  • Sentinel direkomendasikan untuk dibandingkan dengan is dan dievaluasi sebagai truthy, copy.copy() dan copy.deepcopy() mengembalikan objek yang sama, dan jika bisa diimpor berdasarkan nama dari modul, identitasnya tetap terjaga bahkan setelah pickling
  • Sistem tipe memungkinkan sentinel itu sendiri digunakan dalam ekspresi tipe seperti int | MISSING, dan dokumentasi resmi terbaru tersedia di dokumen Python 3.15 untuk sentinel")

Latar belakang

  • Nilai sentinel (sentinel value), yaitu nilai placeholder yang unik, digunakan untuk nilai default saat argumen fungsi tidak diberikan, nilai kembalian yang menandakan pencarian gagal, atau nilai yang menunjukkan data hilang
  • Python memang memiliki nilai khusus None yang biasanya dipakai untuk tujuan ini, tetapi dalam konteks ketika None sendiri adalah nilai yang valid, diperlukan nilai sentinel terpisah yang bisa dibedakan dari None
  • Pada Mei 2021, milis python-dev membahas cara yang lebih baik untuk mengimplementasikan nilai sentinel yang digunakan di traceback.print_exception
  • Implementasi yang ada menggunakan idiom umum _sentinel = object(), tetapi repr-nya terlalu panjang dan kurang informatif sehingga signature fungsi menjadi sulit dibaca
    >>> help(traceback.print_exception)  
    Help on function print_exception in module traceback:  
    
    print_exception(exc, /, value=<object object at  
    0x000002825DF09650>, tb=<object object at 0x000002825DF09650>,  
    limit=None, file=None, chain=True)  
    
  • Dalam proses diskusi, teridentifikasi juga masalah lain pada implementasi sentinel yang ada
    • Beberapa sentinel tidak memiliki tipe yang unik, sehingga sulit mendefinisikan signature tipe yang jelas untuk fungsi yang menggunakan sentinel sebagai nilai default
    • Setelah disalin, instance terpisah dapat terbentuk sehingga perbandingan is gagal dan perilakunya tidak sesuai harapan
    • Beberapa idiom umum juga mengalami masalah serupa setelah di-pickle lalu di-unpickle
  • Victor Stinner menyediakan daftar nilai sentinel yang digunakan di Python standard library, dan terkonfirmasi bahwa bahkan di dalam standard library sendiri ada beberapa cara implementasi berbeda, dengan banyak implementasi memiliki satu atau lebih masalah di atas
  • Pemungutan suara di discuss.python.org tidak menghasilkan kesimpulan yang jelas berdasarkan 39 suara
    • 40% memilih “keadaan saat ini sudah baik dan tidak perlu konsistensi”
    • Mayoritas memilih satu atau lebih solusi yang distandardisasi
    • 37% memilih opsi “menggunakan factory/class/metaclass sentinel khusus baru secara konsisten, dan menyediakannya secara publik di standard library”
  • Karena hasilnya terpecah, PEP pun ditulis, dan menghasilkan kesimpulan bahwa implementasi standard library yang sederhana dan baik akan berguna baik di dalam maupun di luar standard library
  • Mengganti semua sentinel yang ada di standard library dengan cara ini bukanlah syarat wajib, dan diserahkan pada kebijakan masing-masing maintainer
  • Dokumen PEP adalah dokumen historis, dan dokumentasi resmi terbaru tersedia di dokumen Python 3.15 untuk sentinel")

Kriteria desain

  • Saat dibandingkan dengan operator is, objek sentinel harus selalu identik dengan dirinya sendiri, dan tidak identik dengan objek lain mana pun
  • Pembuatan objek sentinel harus berupa satu baris kode yang sederhana dan intuitif
  • Harus mudah mendefinisikan beberapa nilai sentinel berbeda sebanyak yang diperlukan
  • Objek sentinel harus memiliki repr yang singkat dan jelas
  • Harus bisa menggunakan signature tipe yang jelas untuk sentinel
  • Harus tetap berfungsi dengan benar setelah penyalinan, serta memiliki perilaku yang dapat diprediksi saat pickling dan unpickling
  • Harus berjalan di CPython 3.x dan PyPy3, dan jika memungkinkan juga di implementasi Python lainnya
  • Baik implementasi maupun penggunaannya harus sesederhana dan seintuitif mungkin, dan tidak menjadi satu lagi konsep khusus yang membebani saat mempelajari Python
  • Standard library tidak bisa bergantung pada implementasi paket PyPI seperti sentinels atau sentinel, sehingga dibutuhkan implementasi yang bisa digunakan di dalam standard library sendiri

Spesifikasi sentinel()

  • Objek callable bawaan baru sentinel ditambahkan
    >>> MISSING = sentinel('MISSING')  
    >>> MISSING  
    MISSING  
    
  • sentinel() menerima satu argumen positional-only, name, dan name harus berupa str
  • Jika nilai non-string diberikan, TypeError akan muncul
  • name digunakan sebagai nama sentinel dan repr-nya
  • Objek sentinel memiliki dua atribut publik
    • __name__: nama sentinel
    • __module__: nama modul tempat sentinel() dipanggil
  • sentinel tidak dapat di-subclass
  • Setiap pemanggilan sentinel(name) mengembalikan objek sentinel baru
  • Jika sentinel yang sama perlu digunakan di beberapa tempat, objek itu harus ditetapkan ke variabel lalu digunakan ulang secara eksplisit, seperti idiom MISSING = object() yang sudah ada
    MISSING = sentinel('MISSING')  
    
    def read_value(default=MISSING):  
        ...  
    
  • Untuk memeriksa apakah nilai tertentu adalah sentinel, disarankan menggunakan operator is, seperti pada None
  • Perbandingan == juga bekerja sesuai harapan dengan hanya mengembalikan True saat dibandingkan dengan dirinya sendiri
  • Pemeriksaan identitas seperti if value is MISSING: biasanya lebih tepat daripada pemeriksaan boolean seperti if value: atau if not value:
  • Objek sentinel bersifat truthy, dan evaluasi booleannya menghasilkan True
    • Ini sama seperti perilaku default kelas arbitrer dan nilai boolean Ellipsis
    • Ini berbeda dari None, yang bersifat falsy
  • Jika objek sentinel disalin dengan copy.copy() atau copy.deepcopy(), objek yang sama akan dikembalikan
  • Sentinel yang dapat diimpor berdasarkan nama dari modul tempat ia didefinisikan mempertahankan identitasnya setelah pickling dan unpickling melalui mekanisme pickle standar
    MISSING = sentinel('MISSING')  
    assert pickle.loads(pickle.dumps(MISSING)) is MISSING  
    
  • sentinel() mencatat modul pemanggil ke atribut __module__ saat sentinel dibuat
  • Pickling mencatat sentinel berdasarkan modul dan nama, dan unpickling mengimpor modul lalu mengambil sentinel berdasarkan nama
  • Sentinel yang tidak dapat diimpor berdasarkan modul dan nama, seperti sentinel yang dibuat di scope lokal dan tidak ditetapkan ke nama yang cocok pada global modul atau atribut kelas, tidak dapat di-pickle
  • repr dari objek sentinel adalah name yang diberikan ke sentinel(), tanpa qualifier modul implisit
  • Jika repr yang terqualifikasi diperlukan, itu harus disertakan secara eksplisit dalam nama
    >>> MyClass_NotGiven = sentinel('MyClass.NotGiven')  
    >>> MyClass_NotGiven  
    MyClass.NotGiven  
    
  • Perbandingan urutan untuk objek sentinel tidak didefinisikan
  • Sentinel tidak mendukung weakref

Pengetikan

  • Untuk membuat penggunaan sentinel dalam kode Python bertipe lebih jelas dan sederhana, penanganan khusus untuk objek sentinel ditambahkan ke sistem tipe
  • Objek sentinel dapat digunakan sebagai nilai yang merepresentasikan dirinya sendiri di dalam ekspresi tipe")
  • Ini serupa dengan cara None ditangani dalam sistem tipe yang ada
    MISSING = sentinel('MISSING')  
    
    def foo(value: int | MISSING = MISSING) -> int:  
        ...  
    
  • Type checker harus mengenali pembuatan sentinel dalam bentuk NAME = sentinel('NAME') sebagai pembuatan objek sentinel baru
  • Jika nama yang diberikan ke sentinel() tidak cocok dengan nama target assignment, type checker harus menghasilkan error
  • Sentinel yang didefinisikan dengan sintaks ini dapat digunakan dalam ekspresi tipe")
  • Tipe sentinel tersebut merepresentasikan tipe statis penuh") yang hanya memiliki satu anggota, yaitu objek sentinel itu sendiri
  • Type checker harus mendukung narrowing union type yang menyertakan sentinel dengan menggunakan operator is dan is not
    from typing import assert_type  
    
    MISSING = sentinel('MISSING')  
    
    def foo(value: int | MISSING) -> None:  
        if value is MISSING:  
            assert_type(value, MISSING)  
        else:  
            assert_type(value, int)  
    
  • Implementasi runtime harus memiliki metode __or__ dan __ror__ untuk mendukung penggunaan dalam ekspresi tipe, dan metode ini mengembalikan objek typing.Union")
  • Typing Council mendukung bagian terkait tipe dari proposal ini

C API

  • Karena sentinel juga dapat berguna di ekstensi C, dua fungsi C API baru diusulkan
  • PyObject *PySentinel_New(const char *name, const char *module_name) membuat objek sentinel baru
  • bool PySentinel_Check(PyObject *obj) memeriksa apakah objek adalah sentinel
  • Kode C dapat menggunakan operator == untuk memeriksa apakah suatu objek adalah sentinel tertentu

Kompatibilitas dan keamanan

  • Menambahkan nama bawaan baru berarti kode yang saat ini mengasumsikan bare name sentinel akan memunculkan NameError tidak lagi melihat hasil yang sama
  • Ini adalah pertimbangan kompatibilitas yang umum saat menambahkan nama bawaan baru
  • Nama lokal, global, atau impor sentinel yang sudah ada tidak akan terpengaruh
  • Kode yang sudah menggunakan nama sentinel mungkin perlu disesuaikan agar menggunakan objek bawaan baru, dan mungkin menerima peringatan baru dari linter yang memperingatkan konflik dengan nama bawaan
  • Docstring, dokumentasi library, dan bagian “What’s New” dianggap cukup sebagai cara dokumentasi umum untuk fitur bawaan baru
  • Proposal ini dianggap tidak memiliki implikasi keamanan

Implementasi referensi dan backport

  • Implementasi referensi disediakan sebagai pull request CPython [10]
  • Implementasi referensi sebelumnya ada di repositori GitHub terpisah [7]
  • Sketsa perilaku yang dimaksud adalah sebagai berikut
    class sentinel:  
        &quot;&quot;&quot;Unique sentinel values.&quot;&quot;&quot;  
    
        __slots__ = (&quot;__name__&quot;, &quot;_module_name&quot;)  
    
        def __init_subclass__(cls):  
            raise TypeError(&quot;type &#039;sentinel&#039; is not an acceptable base type&quot;)  
    
        def __init__(self, name, /):  
            if not isinstance(name, str):  
                raise TypeError(&quot;sentinel name must be a string&quot;)  
            self.__name__ = name  
            self._module_name = sys._getframemodulename(1)  
    
        @property  
        def __module__(self):  
            return self._module_name  
    
        def __repr__(self):  
            return self.__name__  
    
        def __reduce__(self):  
            return self.__name__  
    
        def __copy__(self):  
            return self  
    
        def __deepcopy__(self, memo):  
            return self  
    
        def __or__(self, other):  
            return typing.Union[self, other]  
    
        def __ror__(self, other):  
            return typing.Union[other, self]  
    
    • Modul typing-extensions memiliki backport, tetapi saat ini belum sepenuhnya cocok dengan perilaku dalam revisi PEP saat ini

Alternatif yang ditolak

  • Menggunakan NotGiven = object()

    • Pendekatan ini memiliki semua kelemahan yang dibahas dalam kriteria desain PEP
    • repr-nya panjang dan tidak jelas, sulit memperjelas tanda tangan tipe, dan dapat menimbulkan masalah terkait penyalinan atau pickling
  • Menambahkan satu nilai sentinel baru tunggal seperti MISSING atau Sentinel

    • Jika satu nilai dipakai di banyak tempat untuk banyak tujuan, pada beberapa kasus penggunaan sulit untuk selalu yakin bahwa nilai itu sendiri bukan nilai yang valid
    • Nilai sentinel khusus yang berbeda-beda dapat digunakan dengan lebih percaya diri tanpa harus mempertimbangkan edge case potensial
    • Nilai sentinel harus dapat memberikan nama dan repr yang bermakna sesuai konteks penggunaannya
    • Opsi ini sangat tidak populer, hanya dipilih oleh 12% suara dalam pemungutan suara
  • Menggunakan nilai sentinel Ellipsis yang sudah ada

    • Ellipsis pada dasarnya tidak dimaksudkan untuk tujuan seperti ini
    • Penggunaannya memang makin sering untuk mendefinisikan blok kelas atau fungsi kosong alih-alih pass, tetapi tetap tidak bisa dipakai dengan keyakinan yang sama di semua kasus seperti nilai sentinel khusus yang berbeda
  • Menggunakan Enum bernilai tunggal

    • Idiom yang diusulkan adalah sebagai berikut
    class NotGivenType(Enum):  
      NotGiven = &#039;NotGiven&#039;  
      NotGiven = NotGivenType.NotGiven  
    
  • Terlalu berulang, dan repr-nya terlalu panjang seperti &lt;NotGivenType.NotGiven: &#039;NotGiven&#039;&gt;
  • repr yang lebih pendek memang bisa didefinisikan, tetapi kode dan pengulangannya menjadi lebih banyak
  • Ini menjadi opsi yang paling tidak populer karena satu-satunya dari 9 opsi dalam pemungutan suara yang tidak menerima suara sama sekali
  • Dekorator kelas sentinel

    • Idiom yang diusulkan adalah sebagai berikut
      @sentinel  
      class NotGivenType: pass  
      NotGiven = NotGivenType()  
      
    • Implementasi dekorator itu sendiri bisa sederhana dan jelas, tetapi idiomnya terlalu bertele-tele, repetitif, dan sulit diingat
  • Menggunakan objek kelas

    • Karena kelas pada dasarnya adalah singleton, gagasan untuk memakainya sebagai nilai sentinel memang memungkinkan
    • Bentuk paling sederhananya adalah sebagai berikut
      class NotGiven: pass  
      
      • Untuk mendapatkan repr yang jelas, diperlukan metaclass atau dekorator kelas
      class NotGiven(metaclass=SentinelMeta): pass  
      
      @Sentinel  
      class NotGiven: pass  
      
    • Menggunakan kelas dengan cara seperti ini tidak lazim sehingga bisa membingungkan
    • Tanpa komentar, sulit memahami maksud kode tersebut, dan muncul perilaku tak terduga yang tidak diinginkan seperti sentinel menjadi dapat dipanggil
  • Mendefinisikan hanya idiom standar yang direkomendasikan tanpa implementasi

    • Sebagian besar idiom lama yang umum memiliki kelemahan penting
    • Sampai sekarang belum ditemukan idiom yang jelas dan ringkas sambil menghindari kelemahan-kelemahan tersebut
    • Dalam pemungutan suara terkait, opsi rekomendasi idiom tidak populer, dan bahkan opsi dengan suara terbanyak pun hanya mencapai 25%
  • Menggunakan modul pustaka standar baru

    • Draf awal mengusulkan penambahan kelas Sentinel ke modul baru sentinels atau sentinellib
    • Menambahkan modul baru hanya untuk satu callable publik tidak diperlukan
    • Penggunaan modul juga membuat fitur ini lebih tidak praktis dibanding idiom object() yang sudah ada
    • Steering Council juga secara khusus merekomendasikan agar ini dijadikan fitur bawaan sehingga semudah object() untuk digunakan
    • Nama sentinels sudah bertabrakan dengan paket PyPI yang aktif digunakan, dan menjadikannya fitur bawaan menghindari masalah penamaan itu
  • Menggunakan registri nama sentinel per modul

    • Draf awal mengusulkan agar nama sentinel dibuat unik di dalam modul
    • Dalam desain ini, pemanggilan berulang sentinel("MISSING") di modul yang sama akan mengembalikan objek yang sama melalui registri global proses dengan nama modul dan nama sentinel sebagai kunci
    • Perilaku ini ditolak karena terlalu implisit
    • Jika sentinel bersama diperlukan, cukup definisikan satu secara eksplisit seperti MISSING = object() yang sudah ada lalu gunakan kembali berdasarkan namanya
    • Dalam scope lokal, mungkin justru diinginkan sentinel baru pada setiap pemanggilan atau pengulangan, sehingga pemanggilan berulang sentinel(name) harus menghasilkan objek yang berbeda, seperti pemanggilan berulang object()
    • Dengan menghapus registri, implementasi dan model mentalnya menjadi lebih sederhana, dan yang tersisa hanyalah aturan bahwa sentinel(name) membuat objek unik baru dengan repr berupa name
  • Penemuan atau penerusan nama modul secara otomatis

    • Draf awal mengusulkan argumen opsional module_name untuk mendukung desain berbasis registri
    • Setelah registri dihapus, argumen publik module_name tidak lagi diperlukan dalam usulan inti
    • Implementasinya secara internal tetap mencatat modul pemanggil agar pickling dapat menserialisasi sentinel yang dapat diimpor berdasarkan modul dan nama, mirip TypeVar
    • Nama modul internal tidak memengaruhi repr sentinel
    • Jika menginginkan repr yang menyertakan nama modul atau nama kelas, itu bisa dimasukkan secara eksplisit dalam satu argumen name, misalnya sentinel("mymodule.MISSING")
  • Mengizinkan kustomisasi repr

    • Ini punya kelebihan karena nilai sentinel yang sudah ada bisa dipindahkan ke pendekatan ini tanpa mengubah repr
    • Namun, ini dikecualikan karena dianggap tidak sepadan dengan kompleksitas tambahan yang dibawanya
  • Mengizinkan kustomisasi evaluasi boolean

    • Dalam diskusi, dipertimbangkan cara agar sentinel bisa secara eksplisit dibuat truthy, falsy, atau tidak dapat dikonversi ke bool
    • Beberapa sentinel pihak ketiga memang menyediakan perilaku falsy sebagai bagian dari API publik mereka
    • Sejumlah peserta berpendapat bahwa melempar pengecualian dalam konteks boolean lebih baik untuk memaksa pemeriksaan identitas
    • PEP menyederhanakan usulan awal dengan mempertahankan perilaku truthy bawaan objek biasa dan merekomendasikan pemeriksaan identitas
    • Perilaku boolean kustom dapat dipertimbangkan nanti jika dinilai layak dengan tambahan API dan kompleksitas pengetikan yang diperlukan
  • Menggunakan typing.Literal dalam anotasi tipe

    • Beberapa orang mengusulkannya dalam diskusi, dan PEP juga awalnya mengadopsi pendekatan ini
    • Namun, ini bisa menimbulkan kebingungan karena Literal["MISSING"] merujuk ke nilai string "MISSING", bukan referensi maju ke nilai sentinel MISSING
    • Penggunaan bare name juga sering diusulkan dalam diskusi
    • Pendekatan bare name mengikuti preseden yang dibuat oleh None dan pola yang sudah dikenal, tidak memerlukan impor, dan jauh lebih singkat

Panduan penggunaan tambahan

  • Saat mendefinisikan sentinel dalam scope kelas, untuk menghindari benturan nama, atau ketika repr yang terbatasi lebih jelas, nama terbatasi yang diinginkan harus diteruskan secara eksplisit
    &gt;&gt;&gt; class MyClass:  
    ...    NotGiven = sentinel(&#039;MyClass.NotGiven&#039;)  
    &gt;&gt;&gt; MyClass.NotGiven  
    MyClass.NotGiven  
    
  • Membuat sentinel di dalam fungsi atau metode diperbolehkan
  • Karena setiap pemanggilan sentinel() menghasilkan objek yang berbeda, sentinel yang dibuat di scope lokal akan berperilaku seperti nilai yang dibuat dengan memanggil object() di scope tersebut
  • Nilai boolean NotImplemented adalah True, tetapi penggunaan ini telah deprecated sejak Python 3.9 dan akan memunculkan deprecation warning
  • Depresiasi ini disebabkan oleh masalah khusus NotImplemented yang dijelaskan di bpo-35712 [8]
  • Jika perlu mendefinisikan beberapa nilai sentinel yang saling terkait atau menentukan urutan di antaranya, maka Enum atau pendekatan serupa harus digunakan
  • Untuk pengetikan sentinel semacam ini, beberapa opsi dibahas di mailing list typing-sig [9]

1 komentar

 
GN⁺ 1 jam lalu
Komentar Lobste.rs
  • Nama yang dipilih terasa aneh karena tampaknya maknanya terlalu sempit
    Kalau hanya melihat namanya, rasanya simbol unik akan menjadi primitif yang lebih fleksibel. Memang pada praktiknya ini hampir akan berperilaku seperti simbol, jadi bisa dipakai seperti itu, tetapi menamainya “Sentinels” terasa janggal. Mungkin ini karena saya terbiasa dengan Lisp

    • Sepertinya tujuannya adalah membuat SENTINEL_A menjadi tipe yang berbeda dari SENTINEL_B, sehingga kita bisa menanyakan apakah suatu nilai is_a SENTINEL_A
      Simbol di Ruby tidak bekerja seperti itu: :beef.is_a? :droog.class #=> true
    • Cara berpikir ala Lisp memang masuk akal. Itu berangkat dari asumsi bahwa penggunaan yang luas itu diinginkan dan merupakan masalah yang perlu diselesaikan, tetapi di Python sudah ada Literal dan string literal untuk sebagian besar kasus penggunaan simbol Lisp
      Alasan ini disebut sentinel bernama adalah karena sentinel values merupakan konsep dan pola yang umum di Python, dan sentinel dimaksudkan untuk menyelesaikan secara sempit sebagian masalah yang muncul saat memakai pola itu. Persis seperti yang dijelaskan di bagian “Motivation” dan “Rationale”
      Selain itu, sentinel tidak memiliki semantik nilai, jadi dua sentinel dengan nama yang sama pun tetap merupakan nilai yang berbeda dan tidak sama satu sama lain. Jadi ini juga tidak berperilaku seperti simbol, dan sebaiknya tidak dipakai seperti itu
  • Untuk masalah nilai default pada argumen bernama, di Typst cukup menambahkan nilai auto di samping none untuk mengekspresikan hampir semua antarmuka argumen bernama yang diinginkan
    none saja tidak cocok secara makna untuk sebagian besar nilai default argumen bernama. none bagus sebagai nilai return default, tetapi saat masuk sebagai argumen fungsi sering kali tidak membawa makna yang tepat sebagai kata benda. matrix(axes=None) ambigu: apakah itu berarti menghapus sumbu, atau mempertahankannya seperti biasa? Juga tidak jelas apakah memberikan none berbeda dengan tidak memberikan apa pun. Jika beralih ke multiple dispatch untuk membedakan ada tidaknya parameter, kita kehilangan satu tempat utama untuk mendokumentasikan perilaku parameter tersebut
    auto adalah nilai default yang sangat baik karena langsung bermakna “tangani dengan semestinya berdasarkan informasi yang ada”. Signature auto | none bisa dipakai seperti boolean yang lebih eksplisit, dan T | auto | none memberi cukup banyak informasi tentang bagaimana fungsi akan memakai nilainya. Misalnya jika T adalah color, maka auto kemungkinan berarti memilih default seperti putih/hitam atau mewarisi dari induk, T berarti menetapkan warna secara eksplisit, dan none tergantung konteks bisa berarti tidak menetapkan warna sama sekali atau memperlakukannya sebagai transparan

  • Menarik, dan saya penasaran bagaimana semantik beberapa paket akan berubah. Misalnya, alih-alih mengembalikan Item | None, bisa menulis seperti ini

    NOT_FOUND = sentinel("NOT_FOUND")  
    def get_item(iid: str) -> Item | NOT_FOUND: ...  
    

    Tentu saja, beberapa sentinel juga bisa dipakai untuk membawa makna tambahan. Ini sebenarnya sudah mungkin dari dulu, tetapi belum ada cara yang “direkomendasikan secara resmi” dalam dokumentasi. Ini bisa saja mengarahkan penulis paket ke arah yang berbeda

    MISSING_ID = sentinel("MISSING_ID")  
    MISSING_VALUE = sentinel("MISSING_VALUE")
    
    def get_item(iid: str) -> Item | MISSING_ID | MISSING_VALUE: ...  
    

    Memang contoh yang agak dipaksakan, tetapi dalam kasus ini kita bisa membedakan antara kegagalan karena ID itu ada tetapi tidak punya nilai terkait, dan kegagalan karena ID semacam itu memang tidak ada. Cara yang mungkin lebih “Pythonic” barangkali memakai exception, tetapi ini terlihat lebih seperti pendekatan fungsional daripada gaya Python yang biasa ditulis

    • Dulu ini tampak seperti cara yang lebih rapi untuk memakai singleton yang dibuat dengan kelas dummy lalu diinstansiasi per modul
      class _MissingId: ...
      
      MISSING_ID = _MissingId()
      
      # elsewhere  
      from ... import MISSING_ID  
      
      Ini mengingatkan pada Symbols
    • Di PEP disebutkan bahwa jika ingin mendefinisikan beberapa nilai sentinel yang saling terkait, atau bahkan memberi urutan di antaranya, sebaiknya gunakan Enum atau sesuatu yang serupa
  • Sepertinya akan lebih baik kalau langsung mengadopsi API Symbol milik JavaScript. Itu berguna secara umum, dan juga menyelesaikan masalah yang ingin dipecahkan di sini