Skip to main content

compat-sentinel

PEP 661 sentinel for Python 3.10 and later.

On Python 3.15 and later, compat_sentinel.sentinel is the builtin. On older interpreters it is a local implementation with the same constructor, copy behavior, and pickle lookup.

from compat_sentinel import sentinel

MISSING = sentinel("MISSING")
DEFAULT = sentinel("DEFAULT", repr="<default>")

Each call returns a new object. A sentinel pickles back to itself when it can be imported from its module under __name__:

MISSING = sentinel("MISSING")

class Box:
    SHORT = sentinel("Box.SHORT")

A sentinel created in a local scope and never stored under that name does not pickle. __module__ is taken from the caller and is writable; pickle uses the value present at dump time.

int | MISSING and MISSING | str build a typing.Union.

pythonbackport-sentinel

pythonbackport-sentinel 0.1.0 claims the same 3.15 sentinel contract. Its current master does not meet it. Construction, repr, truthiness, identity equality, and copy do work.

  • The caller lookup stops on sentinel.__new__. Every sentinel reports __module__ == "sentinel.core" and a qualified name of sentinel.__new__.
  • Pickle identity is a process-wide registry keyed by that module, that qualified name, and the sentinel name. Two sentinel("MISSING") values share one slot, and the later one replaces the earlier one. Unpickling the first returns the second, including in a fresh process after the defining module has been imported.
  • __reduce__ rebuilds through a private function. A new process unpickles a sentinel without importing the module that defined it, and a custom repr is dropped. A local sentinel that was never stored under its name still pickles.
  • Assigning __module__ changes the attribute and leaves the pickle key unchanged.
  • int | sentinel("A") raises TypeError on Python 3.13 because types.UnionType is not subscriptable. The same subscript is used on every version before 3.14. On 3.14 the expression returns a typing.Union.
  • The package never uses builtins.sentinel. On 3.15, from sentinel import sentinel is still this class, so its pickles are not the builtin module-and-name lookup.

tests/test_contract.py loads one implementation per run. The default is compat_sentinel. --sentinel=pybp loads pythonbackport-sentinel. On Python 3.13 the first command passes and the second fails all eight contract checks:

uv run --with pytest pytest -q
uv run --python 3.13 --with pytest --with pythonbackport-sentinel pytest -q --sentinel=pybp tests/test_contract.py
...........................                                                                [100%]
27 passed in 0.18s
FFFFFFFF                                                                                   [100%]
============================================ FAILURES ============================================
___________________________ test_caller_module_is_the_defining_module ____________________________

subjects = <module 'contract_subjects' from '.../contract_subjects.py'>

    def test_caller_module_is_the_defining_module(subjects) -> None:
>       assert subjects.MISSING.__module__ == subjects.__name__
E       AssertionError: assert 'sentinel.core' == 'contract_subjects'
E
E         - contract_subjects
E         + sentinel.core

tests/test_contract.py:71: AssertionError
_________________________ test_same_name_keeps_distinct_pickle_identity __________________________

subjects = <module 'contract_subjects' from '.../contract_subjects.py'>

    def test_same_name_keeps_distinct_pickle_identity(subjects) -> None:
        restored = pickle.loads(pickle.dumps(subjects.MISSING))

>       assert restored is subjects.MISSING
E       AssertionError: assert MISSING is MISSING
E        +  where MISSING = <module 'contract_subjects' from '.../contract_subjects.py'>.MISSING

tests/test_contract.py:77: AssertionError
_________________________________ test_pickle_is_a_module_lookup _________________________________

subjects = <module 'contract_subjects' from '.../contract_subjects.py'>

    def test_pickle_is_a_module_lookup(subjects) -> None:
        blob = pickle.dumps(subjects.MISSING)

>       assert subjects.__name__.encode() in blob
E       AssertionError: assert b'contract_subjects' in b'\x80\x04\x95X\x00\x00\x00\x00\x00\x00\x00\x8c\rsentinel.core\x94\x8c\x15_reconstruct_sentinel\x94\x93\x94\x8c&sentinel.core\x00sentinel.__new__\x00MISSING\x94\x85\x94R\x94.'
E        +  where b'contract_subjects' = <built-in method encode of str object at 0x105f29070>()
E        +    where <built-in method encode of str object at 0x105f29070> = 'contract_subjects'.encode
E        +      where 'contract_subjects' = <module 'contract_subjects' from '.../contract_subjects.py'>.__name__

tests/test_contract.py:84: AssertionError
___________________________ test_custom_repr_survives_in_a_new_process ___________________________

subjects = <module 'contract_subjects' from '.../contract_subjects.py'>

    def test_custom_repr_survives_in_a_new_process(subjects) -> None:
        report = _child("with-module", pickle.dumps(subjects.CUSTOM), subjects)

>       assert report.get("is_custom") == "True", report
E       AssertionError: {'is_custom': 'False', 'repr': 'CUSTOM'}
E       assert 'False' == 'True'
E
E         - True
E         + False

tests/test_contract.py:91: AssertionError
___________________________ test_unpickle_requires_the_defining_module ___________________________

subjects = <module 'contract_subjects' from '.../contract_subjects.py'>

    def test_unpickle_requires_the_defining_module(subjects) -> None:
        report = _child("no-module", pickle.dumps(subjects.MISSING), subjects)

>       assert report.get("error") == "ModuleNotFoundError", report
E       AssertionError: {'ok': 'MISSING', 'module': 'sentinel.core'}
E       assert None == 'ModuleNotFoundError'
E        +  where None = <built-in method get of dict object at 0x105fcc240>('error')
E        +    where <built-in method get of dict object at 0x105fcc240> = {'ok': 'MISSING', 'module': 'sentinel.core'}.get

tests/test_contract.py:98: AssertionError
______________________________ test_local_sentinel_is_not_picklable ______________________________

sentinel = <class 'sentinel.core.sentinel'>

    def test_local_sentinel_is_not_picklable(sentinel) -> None:
        value = sentinel("EPHEMERAL")

>       with pytest.raises(pickle.PicklingError):
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       Failed: DID NOT RAISE PicklingError

tests/test_contract.py:104: Failed
_____________________________ test_module_assignment_controls_pickle _____________________________

subjects = <module 'contract_subjects' from '.../contract_subjects.py'>

    def test_module_assignment_controls_pickle(subjects) -> None:
        original = subjects.HOLDER.__module__
        subjects.HOLDER.__module__ = "not.a.real.module"
        try:
>           with pytest.raises(pickle.PicklingError):
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E           Failed: DID NOT RAISE PicklingError

tests/test_contract.py:112: Failed
___________________________ test_union_accepts_sentinel_on_either_side ___________________________

sentinel = <class 'sentinel.core.sentinel'>

    def test_union_accepts_sentinel_on_either_side(sentinel) -> None:
        value = sentinel("UNION")
>       forward = int | value
                  ^^^^^^^^^^^

tests/test_contract.py:120:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
site-packages/sentinel/core.py:276: in __ror__
    return _make_union(other, self)
           ^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

a = <class 'int'>, b = UNION

    def _make_union(a, b):
        """
        Build a ``Union`` instance that can be used in type expressions.

        Tries the runtime ``types.UnionType`` (PEP 604, Python 3.10+)
        subscript syntax first, falling back to ``typing.Union``.  Direct
        construction with ``types.UnionType((a, b))`` is intentionally
        avoided because it raises ``TypeError`` on Python 3.14+ where the
        runtime union class is the same as ``typing.Union`` and explicitly
        forbids manual instantiation.
        """
        # On Python 3.10-3.13 ``types.UnionType`` is a distinct class whose
        # subscript syntax (``UnionType[a, b]``) is the supported way of
        # constructing a runtime union.
        union_type = getattr(_types, "UnionType", None)
        if union_type is not None and union_type is not getattr(
            __import__("typing"), "Union", None
        ):
>           return union_type[a, b]
                   ^^^^^^^^^^^^^^^^
E           TypeError: type 'types.UnionType' is not subscriptable

site-packages/sentinel/core.py:44: TypeError
==================================== short test summary info =====================================
FAILED tests/test_contract.py::test_caller_module_is_the_defining_module - AssertionError: assert 'sentinel.core' == 'contract_subjects'
FAILED tests/test_contract.py::test_same_name_keeps_distinct_pickle_identity - AssertionError: assert MISSING is MISSING
FAILED tests/test_contract.py::test_pickle_is_a_module_lookup - AssertionError: assert b'contract_subjects' in b'\x80\x04\x95X\x00\x00\x00\x00\x00\x00\x00\x8...
FAILED tests/test_contract.py::test_custom_repr_survives_in_a_new_process - AssertionError: {'is_custom': 'False', 'repr': 'CUSTOM'}
FAILED tests/test_contract.py::test_unpickle_requires_the_defining_module - AssertionError: {'ok': 'MISSING', 'module': 'sentinel.core'}
FAILED tests/test_contract.py::test_local_sentinel_is_not_picklable - Failed: DID NOT RAISE PicklingError
FAILED tests/test_contract.py::test_module_assignment_controls_pickle - Failed: DID NOT RAISE PicklingError
FAILED tests/test_contract.py::test_union_accepts_sentinel_on_either_side - TypeError: type 'types.UnionType' is not subscriptable
8 failed in 0.11s

Metadata

Release files for compat-sentinel 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for compat-sentinel 0.1.2
File Size Uploaded
compat_sentinel-0.1.2.tar.gz 14.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for compat-sentinel 0.1.2
File Interpreter ABI Platform
compat_sentinel-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 21.3 kB

Release files / compat_sentinel-0.1.2.tar.gz

Download URL compat_sentinel-0.1.2.tar.gz
Size 14.1 kB
Tags Source
SHA-256 checksum
How to use checksums
7ff8db6e6f9aaef286de6801dfeb62e8291770f19a3491d2bb8c95d3e62cf32f
BLAKE2b-256 checksum
How to use checksums
87f996c20d278fd6be0073f3362fcf8bf6426e449c2ea6299806c3fbeb33a4e9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.

Transparency log

Release files / compat_sentinel-0.1.2-py3-none-any.whl

Download URL compat_sentinel-0.1.2-py3-none-any.whl
Size 7.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c211bb0b34b0ac808118196b49cec3cc1e7b4c1ad8a414ac11d7aafba963aa2a
BLAKE2b-256 checksum
How to use checksums
1a878a9c9007cf06726823551b1dad31ebba8e47c26934bb372696c9705de288
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page