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 ofsentinel.__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 customrepris 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")raisesTypeErroron Python 3.13 becausetypes.UnionTypeis not subscriptable. The same subscript is used on every version before 3.14. On 3.14 the expression returns atyping.Union.- The package never uses
builtins.sentinel. On 3.15,from sentinel import sentinelis 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)
| File | Size | Uploaded | |
|---|---|---|---|
| compat_sentinel-0.1.2.tar.gz | 14.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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