From 43382e5383514240d625798d70ac032edb7a8fe7 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 2 Dec 2025 13:47:35 -0800 Subject: [PATCH 01/11] Support for pickling sentinel objects as singletons The `repr` parameters position was replaced by `module_name` to conform to PEP 661. Added copy and pickle tests. Updated documentation for Sentinel. `_marker` was defined before `caller` which causes minor issues, resolved by setting its module name manually. --- doc/index.rst | 12 +++++++++++- src/test_typing_extensions.py | 29 ++++++++++++++++++++++------- src/typing_extensions.py | 15 ++++++++++++--- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 66577ef0..061cdd03 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1071,11 +1071,14 @@ Capsule objects Sentinel objects ~~~~~~~~~~~~~~~~ -.. class:: Sentinel(name, repr=None) +.. class:: Sentinel(name, module_name=None, *, repr=None) A type used to define sentinel values. The *name* argument should be the name of the variable to which the return value shall be assigned. + *module_name* is the module where the sentinel is defined. + Defaults to the current modules ``__name__``. + If *repr* is provided, it will be used for the :meth:`~object.__repr__` of the sentinel object. If not provided, ``""`` will be used. @@ -1091,6 +1094,13 @@ Sentinel objects ... >>> func(MISSING) + Sentinels defined inside a class scope should use a :term:`qualified name`. + + Example:: + + >>> class MyClass: + ... MISSING = Sentinel('MyClass.MISSING') + .. versionadded:: 4.14.0 See :pep:`661` diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f07e1eb0..70b04283 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9541,6 +9541,8 @@ def test_invalid_special_forms(self): class TestSentinels(BaseTestCase): + SENTINEL = Sentinel("TestSentinels.SENTINEL") + def test_sentinel_no_repr(self): sentinel_no_repr = Sentinel('sentinel_no_repr') @@ -9570,13 +9572,26 @@ def test_sentinel_not_callable(self): ): sentinel() - def test_sentinel_not_picklable(self): - sentinel = Sentinel('sentinel') - with self.assertRaisesRegex( - TypeError, - "Cannot pickle 'Sentinel' object" - ): - pickle.dumps(sentinel) + def test_sentinel_copy_identity(self): + self.assertIs(self.SENTINEL, copy.copy(self.SENTINEL)) + self.assertIs(self.SENTINEL, copy.deepcopy(self.SENTINEL)) + + anonymous_sentinel = Sentinel("anonymous_sentinel") + self.assertIs(anonymous_sentinel, copy.copy(anonymous_sentinel)) + self.assertIs(anonymous_sentinel, copy.deepcopy(anonymous_sentinel)) + + def test_sentinel_picklable_qualified(self): + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + self.assertIs(self.SENTINEL, pickle.loads(pickle.dumps(self.SENTINEL, protocol=proto))) + + def test_sentinel_picklable_anonymous(self): + anonymous_sentinel = Sentinel("anonymous_sentinel") # Anonymous sentinel can not be pickled + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.assertRaisesRegex( + pickle.PicklingError, + r"attribute lookup anonymous_sentinel on \w+ failed|not found as \w+.anonymous_sentinel" + ): + self.assertIs(anonymous_sentinel, pickle.loads(pickle.dumps(anonymous_sentinel, protocol=proto))) def load_tests(loader, tests, pattern): import doctest diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 20c331ee..56ea472b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -164,6 +164,9 @@ class Sentinel: *name* should be the name of the variable to which the return value shall be assigned. + *module_name* is the module where the sentinel is defined. + Defaults to the current modules ``__name__``. + *repr*, if supplied, will be used for the repr of the sentinel object. If not provided, "" will be used. """ @@ -171,11 +174,16 @@ class Sentinel: def __init__( self, name: str, + module_name: typing.Optional[str] = None, + *, repr: typing.Optional[str] = None, ): self._name = name self._repr = repr if repr is not None else f'<{name}>' + # For pickling as a singleton: + self.__module__ = module_name if module_name is not None else _caller() + def __repr__(self): return self._repr @@ -193,11 +201,12 @@ def __or__(self, other): def __ror__(self, other): return typing.Union[other, self] - def __getstate__(self): - raise TypeError(f"Cannot pickle {type(self).__name__!r} object") + def __reduce__(self) -> str: + """Reduce this sentinel to a singleton.""" + return self._name # Module is taken from the __module__ attribute -_marker = Sentinel("sentinel") +_marker = Sentinel("sentinel", __name__) # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. From c4ec0b34c56e3afee47c7f9c2d4a2401a816bfbb Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 11 Jan 2026 11:50:10 -0800 Subject: [PATCH 02/11] Change module_name into a keyword-only parameter --- doc/index.rst | 2 +- src/typing_extensions.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 061cdd03..77e11734 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1071,7 +1071,7 @@ Capsule objects Sentinel objects ~~~~~~~~~~~~~~~~ -.. class:: Sentinel(name, module_name=None, *, repr=None) +.. class:: Sentinel(name, *, module_name=None, repr=None) A type used to define sentinel values. The *name* argument should be the name of the variable to which the return value shall be assigned. diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 56ea472b..7e42cf77 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -174,8 +174,8 @@ class Sentinel: def __init__( self, name: str, - module_name: typing.Optional[str] = None, *, + module_name: typing.Optional[str] = None, repr: typing.Optional[str] = None, ): self._name = name @@ -206,7 +206,7 @@ def __reduce__(self) -> str: return self._name # Module is taken from the __module__ attribute -_marker = Sentinel("sentinel", __name__) +_marker = Sentinel("sentinel", module_name=__name__) # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. From da7e31eb83abe23fd6ca26f1f7e0733d049dd402 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Apr 2026 14:54:38 -0700 Subject: [PATCH 03/11] Move _caller above sentinel Sentinels _marker requires _caller in order to remove its module_name --- src/typing_extensions.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 7e42cf77..4db43d52 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -159,6 +159,19 @@ # Added with bpo-45166 to 3.10.1+ and some 3.9 versions _FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__ + +def _caller(depth=1, default='__main__'): + try: + return sys._getframemodulename(depth + 1) or default + except AttributeError: # For platforms without _getframemodulename() + pass + try: + return sys._getframe(depth + 1).f_globals.get('__name__', default) + except (AttributeError, ValueError): # For platforms without _getframe() + pass + return None + + class Sentinel: """Create a unique sentinel object. @@ -647,18 +660,6 @@ def _get_protocol_attrs(cls): return attrs -def _caller(depth=1, default='__main__'): - try: - return sys._getframemodulename(depth + 1) or default - except AttributeError: # For platforms without _getframemodulename() - pass - try: - return sys._getframe(depth + 1).f_globals.get('__name__', default) - except (AttributeError, ValueError): # For platforms without _getframe() - pass - return None - - # `__match_args__` attribute was removed from protocol members in 3.13, # we want to backport this change to older Python versions. # Breakpoint: https://github.com/python/cpython/pull/110683 From 6f26100d88ab3db58d10f3bec5294550ab731a7e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Apr 2026 15:14:46 -0700 Subject: [PATCH 04/11] Update sentinel for PEP 661 changes Rename Sentinel to sentinel, deprecated old name Remove module_name parameter Deprecate subclassing sentinel Enforce correct sentinel parameters using deprecated overloads Rename _name attribute to __name__ Rename sentinel in tests, tests passed before making this change Also add tests for sentinel deprecations --- doc/index.rst | 14 +++++------ src/test_typing_extensions.py | 42 ++++++++++++++++++++------------ src/typing_extensions.py | 45 ++++++++++++++++++++++++++--------- 3 files changed, 68 insertions(+), 33 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 77e11734..7f437ca3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1071,21 +1071,21 @@ Capsule objects Sentinel objects ~~~~~~~~~~~~~~~~ -.. class:: Sentinel(name, *, module_name=None, repr=None) +.. class:: sentinel(name, /, *, repr=None) A type used to define sentinel values. The *name* argument should be the name of the variable to which the return value shall be assigned. - *module_name* is the module where the sentinel is defined. - Defaults to the current modules ``__name__``. - If *repr* is provided, it will be used for the :meth:`~object.__repr__` of the sentinel object. If not provided, ``""`` will be used. + A sentinel is bound to the module it is created within, + sentinels are not equal to similar named sentinels from other modules. + Example:: - >>> from typing_extensions import Sentinel, assert_type - >>> MISSING = Sentinel('MISSING') + >>> from typing_extensions import sentinel, assert_type + >>> MISSING = sentinel('MISSING') >>> def func(arg: int | MISSING = MISSING) -> None: ... if arg is MISSING: ... assert_type(arg, MISSING) @@ -1099,7 +1099,7 @@ Sentinel objects Example:: >>> class MyClass: - ... MISSING = Sentinel('MyClass.MISSING') + ... MISSING = sentinel('MyClass.MISSING') .. versionadded:: 4.14.0 diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 70b04283..cf50fae2 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -102,6 +102,7 @@ reveal_type, runtime, runtime_checkable, + sentinel, type_repr, ) @@ -9541,42 +9542,42 @@ def test_invalid_special_forms(self): class TestSentinels(BaseTestCase): - SENTINEL = Sentinel("TestSentinels.SENTINEL") + SENTINEL = sentinel("TestSentinels.SENTINEL") def test_sentinel_no_repr(self): - sentinel_no_repr = Sentinel('sentinel_no_repr') + sentinel_no_repr = sentinel('sentinel_no_repr') - self.assertEqual(sentinel_no_repr._name, 'sentinel_no_repr') - self.assertEqual(repr(sentinel_no_repr), '') + self.assertEqual(sentinel_no_repr.__name__, 'sentinel_no_repr') + self.assertEqual(repr(sentinel_no_repr), 'sentinel_no_repr') def test_sentinel_explicit_repr(self): - sentinel_explicit_repr = Sentinel('sentinel_explicit_repr', repr='explicit_repr') + sentinel_explicit_repr = sentinel('sentinel_explicit_repr', repr='explicit_repr') self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr') @skipIf(sys.version_info < (3, 10), reason='New unions not available in 3.9') def test_sentinel_type_expression_union(self): - sentinel = Sentinel('sentinel') + sentinel_type = sentinel('sentinel') - def func1(a: int | sentinel = sentinel): pass - def func2(a: sentinel | int = sentinel): pass + def func1(a: int | sentinel_type = sentinel_type): pass + def func2(a: sentinel_type | int = sentinel_type): pass - self.assertEqual(func1.__annotations__['a'], Union[int, sentinel]) - self.assertEqual(func2.__annotations__['a'], Union[sentinel, int]) + self.assertEqual(func1.__annotations__['a'], Union[int, sentinel_type]) + self.assertEqual(func2.__annotations__['a'], Union[sentinel_type, int]) def test_sentinel_not_callable(self): - sentinel = Sentinel('sentinel') + sentinel_ = sentinel('sentinel') with self.assertRaisesRegex( TypeError, - "'Sentinel' object is not callable" + "'sentinel' object is not callable" ): - sentinel() + sentinel_() def test_sentinel_copy_identity(self): self.assertIs(self.SENTINEL, copy.copy(self.SENTINEL)) self.assertIs(self.SENTINEL, copy.deepcopy(self.SENTINEL)) - anonymous_sentinel = Sentinel("anonymous_sentinel") + anonymous_sentinel = sentinel("anonymous_sentinel") self.assertIs(anonymous_sentinel, copy.copy(anonymous_sentinel)) self.assertIs(anonymous_sentinel, copy.deepcopy(anonymous_sentinel)) @@ -9585,7 +9586,7 @@ def test_sentinel_picklable_qualified(self): self.assertIs(self.SENTINEL, pickle.loads(pickle.dumps(self.SENTINEL, protocol=proto))) def test_sentinel_picklable_anonymous(self): - anonymous_sentinel = Sentinel("anonymous_sentinel") # Anonymous sentinel can not be pickled + anonymous_sentinel = sentinel("anonymous_sentinel") # Anonymous sentinel can not be pickled for proto in range(pickle.HIGHEST_PROTOCOL + 1): with self.assertRaisesRegex( pickle.PicklingError, @@ -9593,6 +9594,17 @@ def test_sentinel_picklable_anonymous(self): ): self.assertIs(anonymous_sentinel, pickle.loads(pickle.dumps(anonymous_sentinel, protocol=proto))) + def test_sentinel_deprecated(self): + with self.assertWarnsRegex(DeprecationWarning, r"Subclassing sentinel is forbidden by PEP 661"): + class SentinelSubclass(Sentinel): + pass + + with self.assertWarnsRegex(DeprecationWarning, r"Sentinel was renamed to typing_extensions.sentinel"): + my_sentinel = Sentinel(name="my_sentinel") + with self.assertWarnsRegex(DeprecationWarning, r"Setting attributes on sentinel is deprecated"): + my_sentinel.foo = "bar" + + def load_tests(loader, tests, pattern): import doctest tests.addTests(doctest.DocTestSuite(typing_extensions)) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 4db43d52..49ec07ad 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -91,6 +91,7 @@ 'overload', 'override', 'Protocol', + 'sentinel', 'Sentinel', 'reveal_type', 'runtime', @@ -172,30 +173,45 @@ def _caller(depth=1, default='__main__'): return None -class Sentinel: +class sentinel: """Create a unique sentinel object. *name* should be the name of the variable to which the return value shall be assigned. - *module_name* is the module where the sentinel is defined. - Defaults to the current modules ``__name__``. - *repr*, if supplied, will be used for the repr of the sentinel object. If not provided, "" will be used. """ + @overload + def __init__(self, name: str, /, *, repr: typing.Optional[str] = None): ... + + @overload + @deprecated("'name' must be positional-only, 'repr' must be keyword-only.") + def __init__(self, name: str, repr: typing.Optional[str] = None): ... + def __init__( self, name: str, - *, - module_name: typing.Optional[str] = None, repr: typing.Optional[str] = None, ): - self._name = name - self._repr = repr if repr is not None else f'<{name}>' + self.__name__ = name + self._repr = repr if repr is not None else name # For pickling as a singleton: - self.__module__ = module_name if module_name is not None else _caller() + self.__module__ = _caller() + + @deprecated("Subclassing sentinel is forbidden by PEP 661") + def __init_subclass__(cls): + super().__init_subclass__() + + def __setattr__(self, attr: str, value: object) -> None: + if attr not in {"__name__", "_repr", "__module__"}: + warnings.warn( + "Setting attributes on sentinel is deprecated", + DeprecationWarning, + stacklevel=2, + ) + super().__setattr__(attr, value) def __repr__(self): return self._repr @@ -216,10 +232,17 @@ def __ror__(self, other): def __reduce__(self) -> str: """Reduce this sentinel to a singleton.""" - return self._name # Module is taken from the __module__ attribute + return self.__name__ # Module is taken from the __module__ attribute + +with warnings.catch_warnings(): # Allow sentinel subclass for backwards compatibility + warnings.simplefilter("ignore") + + @deprecated("""Sentinel was renamed to typing_extensions.sentinel""") + class Sentinel(sentinel): + pass +_marker = sentinel("sentinel") -_marker = Sentinel("sentinel", module_name=__name__) # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. From ab1c49d934b76d98236966f7fbbe229fdcd5ea90 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Apr 2026 15:51:53 -0700 Subject: [PATCH 05/11] Deprecate sentinel repr parameter `repr` is not in PEP 661 --- doc/index.rst | 7 +++---- src/test_typing_extensions.py | 2 +- src/typing_extensions.py | 8 +++----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 7f437ca3..eb619eac 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1071,17 +1071,16 @@ Capsule objects Sentinel objects ~~~~~~~~~~~~~~~~ -.. class:: sentinel(name, /, *, repr=None) +.. class:: sentinel(name, /) A type used to define sentinel values. The *name* argument should be the name of the variable to which the return value shall be assigned. - If *repr* is provided, it will be used for the :meth:`~object.__repr__` - of the sentinel object. If not provided, ``""`` will be used. - A sentinel is bound to the module it is created within, sentinels are not equal to similar named sentinels from other modules. + Assigning attributes to a sentinel including `__weakref__` is forbidden. + Example:: >>> from typing_extensions import sentinel, assert_type diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index cf50fae2..71ff2bba 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9550,7 +9550,7 @@ def test_sentinel_no_repr(self): self.assertEqual(sentinel_no_repr.__name__, 'sentinel_no_repr') self.assertEqual(repr(sentinel_no_repr), 'sentinel_no_repr') - def test_sentinel_explicit_repr(self): + def test_sentinel_deprecated_explicit_repr(self): sentinel_explicit_repr = sentinel('sentinel_explicit_repr', repr='explicit_repr') self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr') diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 49ec07ad..f6689795 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -177,16 +177,14 @@ class sentinel: """Create a unique sentinel object. *name* should be the name of the variable to which the return value shall be assigned. - - *repr*, if supplied, will be used for the repr of the sentinel object. - If not provided, "" will be used. """ @overload - def __init__(self, name: str, /, *, repr: typing.Optional[str] = None): ... + def __init__(self, name: str, /): ... @overload - @deprecated("'name' must be positional-only, 'repr' must be keyword-only.") + @deprecated("'name' must be positional-only, \ +'repr' is deprecated and must be removed.") def __init__(self, name: str, repr: typing.Optional[str] = None): ... def __init__( From 0a6544bc9814de0ee76f3ce1312e00313f8f1404 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Apr 2026 17:02:59 -0700 Subject: [PATCH 06/11] Skip deprecation of Sentinel class --- src/test_typing_extensions.py | 3 +-- src/typing_extensions.py | 7 +------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 71ff2bba..bf25976a 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9599,8 +9599,7 @@ def test_sentinel_deprecated(self): class SentinelSubclass(Sentinel): pass - with self.assertWarnsRegex(DeprecationWarning, r"Sentinel was renamed to typing_extensions.sentinel"): - my_sentinel = Sentinel(name="my_sentinel") + my_sentinel = Sentinel(name="my_sentinel") with self.assertWarnsRegex(DeprecationWarning, r"Setting attributes on sentinel is deprecated"): my_sentinel.foo = "bar" diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f6689795..935ee035 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -232,12 +232,7 @@ def __reduce__(self) -> str: """Reduce this sentinel to a singleton.""" return self.__name__ # Module is taken from the __module__ attribute -with warnings.catch_warnings(): # Allow sentinel subclass for backwards compatibility - warnings.simplefilter("ignore") - - @deprecated("""Sentinel was renamed to typing_extensions.sentinel""") - class Sentinel(sentinel): - pass +Sentinel = sentinel _marker = sentinel("sentinel") From e306cc1394ec267d8380b1ee0d876d7622a31a0d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Apr 2026 19:40:34 -0700 Subject: [PATCH 07/11] Switch to manual warnings for outdated sentinel parameters --- src/test_typing_extensions.py | 8 +++++-- src/typing_extensions.py | 39 ++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index bf25976a..82182fbf 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9551,7 +9551,8 @@ def test_sentinel_no_repr(self): self.assertEqual(repr(sentinel_no_repr), 'sentinel_no_repr') def test_sentinel_deprecated_explicit_repr(self): - sentinel_explicit_repr = sentinel('sentinel_explicit_repr', repr='explicit_repr') + with self.assertWarnsRegex(DeprecationWarning, r"'repr' is deprecated and must be removed"): + sentinel_explicit_repr = sentinel('sentinel_explicit_repr', repr='explicit_repr') self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr') @@ -9598,8 +9599,11 @@ def test_sentinel_deprecated(self): with self.assertWarnsRegex(DeprecationWarning, r"Subclassing sentinel is forbidden by PEP 661"): class SentinelSubclass(Sentinel): pass + with self.assertRaisesRegex(TypeError, r"First parameter 'name' is required"): + sentinel() - my_sentinel = Sentinel(name="my_sentinel") + with self.assertWarnsRegex(DeprecationWarning, r"'name' is positional-only and must not be a keyword parameter"): + my_sentinel = Sentinel(name="my_sentinel") with self.assertWarnsRegex(DeprecationWarning, r"Setting attributes on sentinel is deprecated"): my_sentinel.foo = "bar" diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 935ee035..ed9b21ee 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -173,27 +173,42 @@ def _caller(depth=1, default='__main__'): return None +# Placeholder for sentinel methods, because sentinels can not have their own sentinels +_sentinel_placeholder = object() + + class sentinel: """Create a unique sentinel object. *name* should be the name of the variable to which the return value shall be assigned. """ - @overload - def __init__(self, name: str, /): ... - - @overload - @deprecated("'name' must be positional-only, \ -'repr' is deprecated and must be removed.") - def __init__(self, name: str, repr: typing.Optional[str] = None): ... - def __init__( self, - name: str, + __name: str = _sentinel_placeholder, + /, repr: typing.Optional[str] = None, - ): - self.__name__ = name - self._repr = repr if repr is not None else name + *, + name: str = _sentinel_placeholder, + ) -> None: + if name is not _sentinel_placeholder: + warnings.warn( + "'name' is positional-only and must not be a keyword parameter", + DeprecationWarning, + stacklevel=2, + ) + __name = name + if __name is _sentinel_placeholder: + raise TypeError("First parameter 'name' is required") + if repr is not None: + warnings.warn( + "'repr' is deprecated and must be removed", + DeprecationWarning, + stacklevel=2, + ) + + self.__name__ = __name + self._repr = repr if repr is not None else __name # For pickling as a singleton: self.__module__ = _caller() From 949eacf075cbbcd1befb18c37e92474b945636b0 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Apr 2026 19:46:24 -0700 Subject: [PATCH 08/11] Switch to manual warning of sentinel subclassing --- src/typing_extensions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index ed9b21ee..36471499 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -213,8 +213,12 @@ def __init__( # For pickling as a singleton: self.__module__ = _caller() - @deprecated("Subclassing sentinel is forbidden by PEP 661") def __init_subclass__(cls): + warnings.warn( + "Subclassing sentinel is forbidden by PEP 661", + DeprecationWarning, + stacklevel=2, + ) super().__init_subclass__() def __setattr__(self, attr: str, value: object) -> None: From cc08d14ce58f9d9fc621d5ec4a6208b2a85fa447 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Apr 2026 20:06:51 -0700 Subject: [PATCH 09/11] Note user-facing changes to sentinel Remove redundant info about sentinels, specifics from PEP 661 do not need to be repeated. --- doc/index.rst | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index eb619eac..49a88e13 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1076,9 +1076,6 @@ Sentinel objects A type used to define sentinel values. The *name* argument should be the name of the variable to which the return value shall be assigned. - A sentinel is bound to the module it is created within, - sentinels are not equal to similar named sentinels from other modules. - Assigning attributes to a sentinel including `__weakref__` is forbidden. Example:: @@ -1093,17 +1090,19 @@ Sentinel objects ... >>> func(MISSING) - Sentinels defined inside a class scope should use a :term:`qualified name`. - - Example:: - - >>> class MyClass: - ... MISSING = sentinel('MyClass.MISSING') - .. versionadded:: 4.14.0 See :pep:`661` + .. versionchanged:: 4.16.0 + + Now supports pickle and will be reduced as a singleton. + Renamed from `Sentinel` to `sentinel`, `Sentinel` is deprecated. + Automatic `repr` string no longer has angle brackets. + `repr` parameter was deprecated. + `name` as a keyword is deprecated. + Subclasssing and attribute assignment are deprecated. + Pure aliases ~~~~~~~~~~~~ From e86435c2fa7a4efaa0491609139c3ee427715ba2 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Apr 2026 21:00:42 -0700 Subject: [PATCH 10/11] Apply suggestions from code review Update related tests Co-authored-by: Jelle Zijlstra --- doc/index.rst | 7 +++++-- src/test_typing_extensions.py | 8 ++++---- src/typing_extensions.py | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 49a88e13..49f2c170 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1076,7 +1076,7 @@ Sentinel objects A type used to define sentinel values. The *name* argument should be the name of the variable to which the return value shall be assigned. - Assigning attributes to a sentinel including `__weakref__` is forbidden. + Assigning attributes to a sentinel is deprecated. Example:: @@ -1096,12 +1096,15 @@ Sentinel objects .. versionchanged:: 4.16.0 + The implementation of this class has been updated to conform to + the accepted version of :pep:`661`. + Now supports pickle and will be reduced as a singleton. Renamed from `Sentinel` to `sentinel`, `Sentinel` is deprecated. Automatic `repr` string no longer has angle brackets. `repr` parameter was deprecated. `name` as a keyword is deprecated. - Subclasssing and attribute assignment are deprecated. + Subclassing and attribute assignment are deprecated. Pure aliases diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 82182fbf..0ba95fc4 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -9551,7 +9551,7 @@ def test_sentinel_no_repr(self): self.assertEqual(repr(sentinel_no_repr), 'sentinel_no_repr') def test_sentinel_deprecated_explicit_repr(self): - with self.assertWarnsRegex(DeprecationWarning, r"'repr' is deprecated and must be removed"): + with self.assertWarnsRegex(DeprecationWarning, r"'repr' parameter is deprecated and will be removed"): sentinel_explicit_repr = sentinel('sentinel_explicit_repr', repr='explicit_repr') self.assertEqual(repr(sentinel_explicit_repr), 'explicit_repr') @@ -9596,15 +9596,15 @@ def test_sentinel_picklable_anonymous(self): self.assertIs(anonymous_sentinel, pickle.loads(pickle.dumps(anonymous_sentinel, protocol=proto))) def test_sentinel_deprecated(self): - with self.assertWarnsRegex(DeprecationWarning, r"Subclassing sentinel is forbidden by PEP 661"): + with self.assertWarnsRegex(DeprecationWarning, r"Subclassing sentinel is deprecated"): class SentinelSubclass(Sentinel): pass with self.assertRaisesRegex(TypeError, r"First parameter 'name' is required"): sentinel() - with self.assertWarnsRegex(DeprecationWarning, r"'name' is positional-only and must not be a keyword parameter"): + with self.assertWarnsRegex(DeprecationWarning, r"Passing 'name' as a keyword argument is deprecated"): my_sentinel = Sentinel(name="my_sentinel") - with self.assertWarnsRegex(DeprecationWarning, r"Setting attributes on sentinel is deprecated"): + with self.assertWarnsRegex(DeprecationWarning, r"Setting attribute 'foo' on sentinel objects is deprecated"): my_sentinel.foo = "bar" diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 36471499..6b8b5ed1 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -193,7 +193,7 @@ def __init__( ) -> None: if name is not _sentinel_placeholder: warnings.warn( - "'name' is positional-only and must not be a keyword parameter", + "Passing 'name' as a keyword argument is deprecated; pass it positionally instead.", DeprecationWarning, stacklevel=2, ) @@ -202,7 +202,7 @@ def __init__( raise TypeError("First parameter 'name' is required") if repr is not None: warnings.warn( - "'repr' is deprecated and must be removed", + "The 'repr' parameter is deprecated and will be removed in Python 3.15.", DeprecationWarning, stacklevel=2, ) @@ -215,7 +215,7 @@ def __init__( def __init_subclass__(cls): warnings.warn( - "Subclassing sentinel is forbidden by PEP 661", + "Subclassing sentinel is deprecated and will be disallowed in Python 3.15", DeprecationWarning, stacklevel=2, ) @@ -224,7 +224,7 @@ def __init_subclass__(cls): def __setattr__(self, attr: str, value: object) -> None: if attr not in {"__name__", "_repr", "__module__"}: warnings.warn( - "Setting attributes on sentinel is deprecated", + f"Setting attribute {attr!r} on sentinel objects is deprecated and will be disallowed in Python 3.15.", DeprecationWarning, stacklevel=2, ) From cf7f6f2e5f0233abb6c589b92949af1f54f259c1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Apr 2026 21:12:57 -0700 Subject: [PATCH 11/11] Port sentinel from Python when available --- src/typing_extensions.py | 127 +++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 60 deletions(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 6b8b5ed1..71b7954d 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -176,80 +176,87 @@ def _caller(depth=1, default='__main__'): # Placeholder for sentinel methods, because sentinels can not have their own sentinels _sentinel_placeholder = object() +if hasattr(builtins, "sentinel"): # 3.15+ + sentinel = builtins.sentinel +else: + class sentinel: + """Create a unique sentinel object. -class sentinel: - """Create a unique sentinel object. - - *name* should be the name of the variable to which the return value shall be assigned. - """ - - def __init__( - self, - __name: str = _sentinel_placeholder, - /, - repr: typing.Optional[str] = None, - *, - name: str = _sentinel_placeholder, - ) -> None: - if name is not _sentinel_placeholder: - warnings.warn( - "Passing 'name' as a keyword argument is deprecated; pass it positionally instead.", - DeprecationWarning, - stacklevel=2, - ) - __name = name - if __name is _sentinel_placeholder: - raise TypeError("First parameter 'name' is required") - if repr is not None: - warnings.warn( - "The 'repr' parameter is deprecated and will be removed in Python 3.15.", - DeprecationWarning, - stacklevel=2, - ) + *name* should be the name of the variable to which the return value + shall be assigned. + """ - self.__name__ = __name - self._repr = repr if repr is not None else __name + def __init__( + self, + __name: str = _sentinel_placeholder, + /, + repr: typing.Optional[str] = None, + *, + name: str = _sentinel_placeholder, + ) -> None: + if name is not _sentinel_placeholder: + warnings.warn( + "Passing 'name' as a keyword argument is deprecated; " + "pass it positionally instead.", + DeprecationWarning, + stacklevel=2, + ) + __name = name + if __name is _sentinel_placeholder: + raise TypeError("First parameter 'name' is required") + if repr is not None: + warnings.warn( + "The 'repr' parameter is deprecated " + "and will be removed in Python 3.15.", + DeprecationWarning, + stacklevel=2, + ) - # For pickling as a singleton: - self.__module__ = _caller() + self.__name__ = __name + self._repr = repr if repr is not None else __name - def __init_subclass__(cls): - warnings.warn( - "Subclassing sentinel is deprecated and will be disallowed in Python 3.15", - DeprecationWarning, - stacklevel=2, - ) - super().__init_subclass__() + # For pickling as a singleton: + self.__module__ = _caller() - def __setattr__(self, attr: str, value: object) -> None: - if attr not in {"__name__", "_repr", "__module__"}: + def __init_subclass__(cls): warnings.warn( - f"Setting attribute {attr!r} on sentinel objects is deprecated and will be disallowed in Python 3.15.", + "Subclassing sentinel is deprecated " + "and will be disallowed in Python 3.15", DeprecationWarning, stacklevel=2, ) - super().__setattr__(attr, value) + super().__init_subclass__() + + def __setattr__(self, attr: str, value: object) -> None: + if attr not in {"__name__", "_repr", "__module__"}: + warnings.warn( + f"Setting attribute {attr!r} on sentinel objects is deprecated " + "and will be disallowed in Python 3.15.", + DeprecationWarning, + stacklevel=2, + ) + super().__setattr__(attr, value) - def __repr__(self): - return self._repr + def __repr__(self): + return self._repr - if sys.version_info < (3, 11): - # The presence of this method convinces typing._type_check - # that Sentinels are types. - def __call__(self, *args, **kwargs): - raise TypeError(f"{type(self).__name__!r} object is not callable") + if sys.version_info < (3, 11): + # The presence of this method convinces typing._type_check + # that Sentinels are types. + def __call__(self, *args, **kwargs): + raise TypeError(f"{type(self).__name__!r} object is not callable") - # Breakpoint: https://github.com/python/cpython/pull/21515 - if sys.version_info >= (3, 10): - def __or__(self, other): - return typing.Union[self, other] + # Breakpoint: https://github.com/python/cpython/pull/21515 + if sys.version_info >= (3, 10): + def __or__(self, other): + return typing.Union[self, other] - def __ror__(self, other): - return typing.Union[other, self] + def __ror__(self, other): + return typing.Union[other, self] - def __reduce__(self) -> str: - """Reduce this sentinel to a singleton.""" - return self.__name__ # Module is taken from the __module__ attribute + def __reduce__(self) -> str: + """Reduce this sentinel to a singleton.""" + return self.__name__ # Module is taken from the __module__ attribute Sentinel = sentinel