Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ New features:
- Add `typing_extensions.Reader` and `typing_extensions.Writer`. Patch by
Sebastian Rittau.
- Fix tests for Python 3.14. Patch by Jelle Zijlstra.
- Add support for sentinels ([PEP 661](https://peps.python.org/pep-0661/)).

# Release 4.13.2 (April 10, 2025)

Expand Down
28 changes: 28 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,34 @@ Capsule objects
.. versionadded:: 4.12.0


Sentinel objects
~~~~~~~~~~~~~~~~

.. 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.

If *repr* is provided, it will be used for the :meth:`~object.__repr__`
of the sentinel object. If not provided, ``"<name>"`` will be used.

Example::

>>> 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)
... else:
... assert_type(arg, int)
...
>>> func(MISSING)

.. versionadded:: 4.14.0

See :pep:`661`


Pure aliases
~~~~~~~~~~~~

Expand Down
40 changes: 40 additions & 0 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
ReadOnly,
Required,
Self,
Sentinel,
Set,
Tuple,
Type,
Expand Down Expand Up @@ -9088,5 +9089,44 @@ def test_invalid_special_forms(self):
self.assertIs(evaluate_forward_ref(typing.ForwardRef("ClassVar", is_argument=False), globals=vars(typing)), ClassVar)


class TestSentinels(BaseTestCase):
def test_sentinel_no_repr(self):
sentinel_no_repr = Sentinel('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')

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')

def func1(a: int | sentinel = sentinel): pass
def func2(a: sentinel | int = sentinel): pass

self.assertEqual(func1.__annotations__['a'], Union[int, sentinel])
self.assertEqual(func2.__annotations__['a'], Union[sentinel, int])

def test_sentinel_not_callable(self):
sentinel = Sentinel('sentinel')
with self.assertRaisesRegex(
TypeError,
"'Sentinel' object is not callable"
):
sentinel()

def test_sentinel_not_picklable(self):
sentinel = Sentinel('sentinel')
with self.assertRaisesRegex(
TypeError,
"Cannot pickle 'Sentinel' object"
):
pickle.dumps(sentinel)


if __name__ == '__main__':
main()
37 changes: 37 additions & 0 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
'overload',
'override',
'Protocol',
'Sentinel',
'reveal_type',
'runtime',
'runtime_checkable',
Expand Down Expand Up @@ -4222,6 +4223,42 @@ def evaluate_forward_ref(
)


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, "<name>" will be used.
"""

def __init__(
self,
name: str,
repr: typing.Optional[str] = None,
):
self._name = name
self._repr = repr if repr is not None else f'<{name}>'

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")

def __or__(self, other):
return Union[self, other]

def __ror__(self, other):
return Union[other, self]

def __getstate__(self):
raise TypeError(f"Cannot pickle {type(self).__name__!r} object")


# Aliases for items that are in typing in all supported versions.
# Explicitly assign these (rather than using `from typing import *` at the top),
# so that we get a CI error if one of these is deleted from typing.py
Expand Down
Loading