From 559e527bc5858848d17287c5916b9e4f2f583e13 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 16 Apr 2026 20:45:16 -0700 Subject: [PATCH 1/9] Initial implementation of PEP 661 v2 (sentinels) --- Doc/library/functions.rst | 48 +++++- Doc/whatsnew/3.15.rst | 14 ++ Include/internal/pycore_sentinelobject.h | 19 +++ Lib/test/test_builtin.py | 62 +++++++ Makefile.pre.in | 1 + ...6-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst | 1 + Objects/clinic/sentinelobject.c.h | 34 ++++ Objects/object.c | 2 + Objects/sentinelobject.c | 161 ++++++++++++++++++ Objects/unionobject.c | 2 + PCbuild/_freeze_module.vcxproj | 1 + PCbuild/_freeze_module.vcxproj.filters | 3 + PCbuild/pythoncore.vcxproj | 2 + PCbuild/pythoncore.vcxproj.filters | 6 + Python/bltinmodule.c | 2 + Tools/c-analyzer/cpython/globals-to-fix.tsv | 1 + 16 files changed, 352 insertions(+), 7 deletions(-) create mode 100644 Include/internal/pycore_sentinelobject.h create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst create mode 100644 Objects/clinic/sentinelobject.c.h create mode 100644 Objects/sentinelobject.c diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 119141d2e6daf3..f7c454797b6e2e 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -19,13 +19,13 @@ are always available. They are listed here in alphabetical order. | | :func:`ascii` | | :func:`filter` | | :func:`map` | | **S** | | | | | :func:`float` | | :func:`max` | | |func-set|_ | | | **B** | | :func:`format` | | |func-memoryview|_ | | :func:`setattr` | -| | :func:`bin` | | |func-frozenset|_ | | :func:`min` | | :func:`slice` | -| | :func:`bool` | | | | | | :func:`sorted` | -| | :func:`breakpoint` | | **G** | | **N** | | :func:`staticmethod` | -| | |func-bytearray|_ | | :func:`getattr` | | :func:`next` | | |func-str|_ | -| | |func-bytes|_ | | :func:`globals` | | | | :func:`sum` | -| | | | | | **O** | | :func:`super` | -| | **C** | | **H** | | :func:`object` | | | +| | :func:`bin` | | |func-frozenset|_ | | :func:`min` | | :func:`sentinel` | +| | :func:`bool` | | | | | | :func:`slice` | +| | :func:`breakpoint` | | **G** | | **N** | | :func:`sorted` | +| | |func-bytearray|_ | | :func:`getattr` | | :func:`next` | | :func:`staticmethod` | +| | |func-bytes|_ | | :func:`globals` | | | | |func-str|_ | +| | | | | | **O** | | :func:`sum` | +| | **C** | | **H** | | :func:`object` | | :func:`super` | | | :func:`callable` | | :func:`hasattr` | | :func:`oct` | | **T** | | | :func:`chr` | | :func:`hash` | | :func:`open` | | |func-tuple|_ | | | :func:`classmethod` | | :func:`help` | | :func:`ord` | | :func:`type` | @@ -1827,6 +1827,40 @@ are always available. They are listed here in alphabetical order. :func:`setattr`. +.. class:: sentinel(name, /) + + Return a new unique sentinel object. *name* must be a :class:`str`, and is + used as the returned object's representation:: + + >>> MISSING = sentinel("MISSING") + >>> MISSING + MISSING + + Sentinel objects are truthy and compare equal only to themselves. They are + intended to be compared with the ``is`` operator. + + Shallow and deep copies of a sentinel object return the object itself. + + Sentinels importable from their defining module by name preserve their + identity when pickled and unpickled. Sentinels that are not importable by + module and name are not picklable. + + Sentinel objects support the ``|`` operator for use in type expressions:: + + def next_value(default: int | MISSING = MISSING): + ... + + .. attribute:: __name__ + + The sentinel's name. + + .. attribute:: __module__ + + The name of the module where the sentinel was created. + + .. versionadded:: next + + .. class:: slice(stop, /) slice(start, stop, step=None, /) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 7ea7c901eceb19..b8cd97d4ed4c6d 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -69,6 +69,8 @@ Summary -- Release highlights ` * :pep:`814`: :ref:`Add frozendict built-in type ` +* :pep:`661`: :ref:`Add sentinel built-in type + ` * :pep:`799`: :ref:`A dedicated profiling package for organizing Python profiling tools ` * :pep:`799`: :ref:`Tachyon: High frequency statistical sampling profiler @@ -234,6 +236,18 @@ to accept also other mapping types such as :class:`~types.MappingProxyType`. (Contributed by Victor Stinner and Donghee Na in :gh:`141510`.) +.. _whatsnew315-sentinel: + +:pep:`661`: Add sentinel built-in type +-------------------------------------- + +A new :class:`sentinel` type is added to the :mod:`builtins` module for +creating unique sentinel values with a concise representation. Sentinel +objects preserve identity when copied, support use in type expressions with +the ``|`` operator, and can be pickled when they are importable by module and +name. + + .. _whatsnew315-profiling-package: :pep:`799`: A dedicated profiling package diff --git a/Include/internal/pycore_sentinelobject.h b/Include/internal/pycore_sentinelobject.h new file mode 100644 index 00000000000000..b8a7f03ab45dd1 --- /dev/null +++ b/Include/internal/pycore_sentinelobject.h @@ -0,0 +1,19 @@ +// Sentinel object interface. + +#ifndef Py_INTERNAL_SENTINELOBJECT_H +#define Py_INTERNAL_SENTINELOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +extern PyTypeObject PySentinel_Type; +#define _PySentinel_Check(op) Py_IS_TYPE((op), &PySentinel_Type) + +#ifdef __cplusplus +} +#endif +#endif // !Py_INTERNAL_SENTINELOBJECT_H diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 844656eb0e2c2e..aad60389384055 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -4,6 +4,7 @@ import builtins import collections import contextlib +import copy import decimal import fractions import gc @@ -52,6 +53,10 @@ # used as proof of globals being used A_GLOBAL_VALUE = 123 +A_SENTINEL = sentinel("A_SENTINEL") + +class SentinelContainer: + CLASS_SENTINEL = sentinel("SentinelContainer.CLASS_SENTINEL") class Squares: @@ -1903,6 +1908,63 @@ class C: __repr__ = None self.assertRaises(TypeError, repr, C()) + def test_sentinel(self): + missing = sentinel("MISSING") + other = sentinel("MISSING") + + self.assertIsInstance(missing, sentinel) + self.assertIs(type(missing), sentinel) + self.assertEqual(missing.__name__, "MISSING") + self.assertEqual(missing.__module__, __name__) + self.assertIsNot(missing, other) + self.assertEqual(repr(missing), "MISSING") + self.assertTrue(missing) + self.assertIs(copy.copy(missing), missing) + self.assertIs(copy.deepcopy(missing), missing) + self.assertEqual(missing, missing) + self.assertNotEqual(missing, other) + self.assertRaises(TypeError, sentinel) + self.assertRaises(TypeError, sentinel, "MISSING", "EXTRA") + self.assertRaises(TypeError, sentinel, name="MISSING") + with self.assertRaisesRegex(TypeError, "must be str"): + sentinel(1) + self.assertTrue(sentinel.__flags__ & support._TPFLAGS_IMMUTABLETYPE) + self.assertFalse(sentinel.__flags__ & support._TPFLAGS_BASETYPE) + with self.assertRaises(TypeError): + class SubSentinel(sentinel): + pass + with self.assertRaises(TypeError): + sentinel.attribute = "value" + with self.assertRaises(AttributeError): + missing.__name__ = "CHANGED" + with self.assertRaises(AttributeError): + missing.__module__ = "changed" + + def test_sentinel_pickle(self): + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol=proto): + self.assertIs( + pickle.loads(pickle.dumps(A_SENTINEL, protocol=proto)), + A_SENTINEL) + self.assertIs( + pickle.loads(pickle.dumps( + SentinelContainer.CLASS_SENTINEL, protocol=proto)), + SentinelContainer.CLASS_SENTINEL) + + missing = sentinel("MISSING") + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(protocol=proto): + with self.assertRaises(pickle.PicklingError): + pickle.dumps(missing, protocol=proto) + + def test_sentinel_union(self): + missing = sentinel("MISSING") + + self.assertEqual((missing | int).__args__, (missing, int)) + self.assertEqual((int | missing).__args__, (int, missing)) + self.assertIs(missing | missing, missing) + self.assertEqual(repr(int | missing), "int | MISSING") + def test_round(self): self.assertEqual(round(0.0), 0.0) self.assertEqual(type(round(0.0)), int) diff --git a/Makefile.pre.in b/Makefile.pre.in index f869c1f7c93776..173cefc402ea9d 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -559,6 +559,7 @@ OBJECT_OBJS= \ Objects/picklebufobject.o \ Objects/rangeobject.o \ Objects/setobject.o \ + Objects/sentinelobject.o \ Objects/sliceobject.o \ Objects/structseq.o \ Objects/templateobject.o \ diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst new file mode 100644 index 00000000000000..9dab491cc7d077 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst @@ -0,0 +1 @@ +Add the :class:`sentinel` built-in class for creating unique sentinel values. diff --git a/Objects/clinic/sentinelobject.c.h b/Objects/clinic/sentinelobject.c.h new file mode 100644 index 00000000000000..51fd35a5979e31 --- /dev/null +++ b/Objects/clinic/sentinelobject.c.h @@ -0,0 +1,34 @@ +/*[clinic input] +preserve +[clinic start generated code]*/ + +#include "pycore_modsupport.h" // _PyArg_CheckPositional() + +static PyObject * +sentinel_new_impl(PyTypeObject *type, PyObject *name); + +static PyObject * +sentinel_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + PyObject *return_value = NULL; + PyTypeObject *base_tp = &PySentinel_Type; + PyObject *name; + + if ((type == base_tp || type->tp_init == base_tp->tp_init) && + !_PyArg_NoKeywords("sentinel", kwargs)) { + goto exit; + } + if (!_PyArg_CheckPositional("sentinel", PyTuple_GET_SIZE(args), 1, 1)) { + goto exit; + } + if (!PyUnicode_Check(PyTuple_GET_ITEM(args, 0))) { + _PyArg_BadArgument("sentinel", "argument 1", "str", PyTuple_GET_ITEM(args, 0)); + goto exit; + } + name = PyTuple_GET_ITEM(args, 0); + return_value = sentinel_new_impl(type, name); + +exit: + return return_value; +} +/*[clinic end generated code: output=7f28fc0bf0259cba input=a9049054013a1b77]*/ diff --git a/Objects/object.c b/Objects/object.c index 3166254f6f640b..2d82fd05c817a8 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -28,6 +28,7 @@ #include "pycore_pyerrors.h" // _PyErr_Occurred() #include "pycore_pymem.h" // _PyMem_IsPtrFreed() #include "pycore_pystate.h" // _PyThreadState_GET() +#include "pycore_sentinelobject.h" // PySentinel_Type #include "pycore_symtable.h" // PySTEntry_Type #include "pycore_template.h" // _PyTemplate_Type _PyTemplateIter_Type #include "pycore_tuple.h" // _PyTuple_DebugMallocStats() @@ -2600,6 +2601,7 @@ static PyTypeObject* static_types[] = { &PySeqIter_Type, &PySetIter_Type, &PySet_Type, + &PySentinel_Type, &PySlice_Type, &PyStdPrinter_Type, &PySuper_Type, diff --git a/Objects/sentinelobject.c b/Objects/sentinelobject.c new file mode 100644 index 00000000000000..820f1c4766836b --- /dev/null +++ b/Objects/sentinelobject.c @@ -0,0 +1,161 @@ +/* Sentinel object implementation */ + +#include "Python.h" +#include "pycore_ceval.h" // _PyThreadState_GET() +#include "pycore_interpframe.h" // _PyFrame_IsIncomplete() +#include "pycore_sentinelobject.h" +#include "pycore_stackref.h" // PyStackRef_AsPyObjectBorrow() +#include "pycore_typeobject.h" // _Py_BaseObject_RichCompare() +#include "pycore_unionobject.h" // _Py_union_from_tuple() +#include "structmember.h" // PyMemberDef + +typedef struct { + PyObject_HEAD + PyObject *name; + PyObject *module; +} sentineldesc; + +#define sentineldesc_CAST(op) \ + (assert(_PySentinel_Check(op)), _Py_CAST(sentineldesc *, (op))) + +/*[clinic input] +class sentinel "sentineldesc *" "&PySentinel_Type" +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=bcb13e08c7eacaee]*/ + +#include "clinic/sentinelobject.c.h" + +static PyObject * +sentinel_get_caller_module(void) +{ + _PyInterpreterFrame *f = _PyThreadState_GET()->current_frame; + while (f && _PyFrame_IsIncomplete(f)) { + f = f->previous; + } + if (f != NULL && !PyStackRef_IsNull(f->f_funcobj)) { + PyObject *module = PyFunction_GetModule( + PyStackRef_AsPyObjectBorrow(f->f_funcobj)); + if (module != NULL && module != Py_None) { + return Py_NewRef(module); + } + PyErr_Clear(); + } + return PyUnicode_FromString("builtins"); +} + +/*[clinic input] +@classmethod +sentinel.__new__ as sentinel_new + + name: object(subclass_of='&PyUnicode_Type') + / +[clinic start generated code]*/ + +static PyObject * +sentinel_new_impl(PyTypeObject *type, PyObject *name) +/*[clinic end generated code: output=4af55c6048bed30d input=3ab75704f39c119c]*/ +{ + Py_INCREF(name); + PyObject *module = sentinel_get_caller_module(); + if (module == NULL) { + Py_DECREF(name); + return NULL; + } + + sentineldesc *self = PyObject_New(sentineldesc, type); + if (self == NULL) { + Py_DECREF(name); + Py_DECREF(module); + return NULL; + } + self->name = name; + self->module = module; + return (PyObject *)self; +} + +static void +sentinel_dealloc(PyObject *op) +{ + sentineldesc *self = sentineldesc_CAST(op); + Py_DECREF(self->name); + Py_DECREF(self->module); + Py_TYPE(op)->tp_free(op); +} + +static PyObject * +sentinel_repr(PyObject *op) +{ + sentineldesc *self = sentineldesc_CAST(op); + return Py_NewRef(self->name); +} + +static PyObject * +sentinel_copy(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return Py_NewRef(self); +} + +static PyObject * +sentinel_deepcopy(PyObject *self, PyObject *Py_UNUSED(memo)) +{ + return Py_NewRef(self); +} + +static PyObject * +sentinel_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) +{ + sentineldesc *self = sentineldesc_CAST(op); + return Py_NewRef(self->name); +} + +static PyObject * +sentinel_or(PyObject *self, PyObject *other) +{ + PyObject *args = PyTuple_Pack(2, self, other); + if (args == NULL) { + return NULL; + } + PyObject *result = _Py_union_from_tuple(args); + Py_DECREF(args); + return result; +} + +static PyMethodDef sentinel_methods[] = { + {"__copy__", sentinel_copy, METH_NOARGS, NULL}, + {"__deepcopy__", sentinel_deepcopy, METH_O, NULL}, + {"__reduce__", sentinel_reduce, METH_NOARGS, NULL}, + {NULL, NULL} +}; + +static PyMemberDef sentinel_members[] = { + {"__name__", Py_T_OBJECT_EX, offsetof(sentineldesc, name), Py_READONLY}, + {"__module__", Py_T_OBJECT_EX, offsetof(sentineldesc, module), Py_READONLY}, + {NULL} +}; + +static PyNumberMethods sentinel_as_number = { + .nb_or = sentinel_or, +}; + +PyDoc_STRVAR(sentinel_doc, +"sentinel(name, /)\n" +"--\n\n" +"Create a unique sentinel object with the given name."); + +PyTypeObject PySentinel_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + .tp_name = "sentinel", + .tp_basicsize = sizeof(sentineldesc), + .tp_dealloc = sentinel_dealloc, + .tp_repr = sentinel_repr, + .tp_as_number = &sentinel_as_number, + .tp_hash = PyObject_GenericHash, + .tp_getattro = PyObject_GenericGetAttr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE, + .tp_doc = sentinel_doc, + .tp_richcompare = _Py_BaseObject_RichCompare, + .tp_methods = sentinel_methods, + .tp_members = sentinel_members, + .tp_new = sentinel_new, + .tp_free = PyObject_Free, +}; diff --git a/Objects/unionobject.c b/Objects/unionobject.c index d33d581f049c5b..57de3217f9f1ea 100644 --- a/Objects/unionobject.c +++ b/Objects/unionobject.c @@ -1,6 +1,7 @@ // typing.Union -- used to represent e.g. Union[int, str], int | str #include "Python.h" #include "pycore_object.h" // _PyObject_GC_TRACK/UNTRACK +#include "pycore_sentinelobject.h" // _PySentinel_Check() #include "pycore_typevarobject.h" // _PyTypeAlias_Type, _Py_typing_type_repr #include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString #include "pycore_unionobject.h" @@ -245,6 +246,7 @@ is_unionable(PyObject *obj) { if (obj == Py_None || PyType_Check(obj) || + _PySentinel_Check(obj) || _PyGenericAlias_Check(obj) || _PyUnion_Check(obj) || Py_IS_TYPE(obj, &_PyTypeAlias_Type)) { diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj index 38236922a523db..0286a5984360ab 100644 --- a/PCbuild/_freeze_module.vcxproj +++ b/PCbuild/_freeze_module.vcxproj @@ -159,6 +159,7 @@ + diff --git a/PCbuild/_freeze_module.vcxproj.filters b/PCbuild/_freeze_module.vcxproj.filters index 73861dbb0c9e7e..a2fc5554c6bd29 100644 --- a/PCbuild/_freeze_module.vcxproj.filters +++ b/PCbuild/_freeze_module.vcxproj.filters @@ -403,6 +403,9 @@ Source Files + + Source Files + Source Files diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 61bee29c0af3d6..a6f546795581c1 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -311,6 +311,7 @@ + @@ -558,6 +559,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 664788e69af19a..0d46ed39cb28db 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -837,6 +837,9 @@ Include\internal + + Include\internal + Include\internal @@ -1274,6 +1277,9 @@ Objects + + Objects + Objects diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index fec64e1ff9d25f..51972b45bbedd6 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -16,6 +16,7 @@ #include "pycore_pyerrors.h" // _PyErr_NoMemory() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_pythonrun.h" // _Py_SourceAsString() +#include "pycore_sentinelobject.h" // PySentinel_Type #include "pycore_tuple.h" // _PyTuple_Recycle() #include "clinic/bltinmodule.c.h" @@ -3554,6 +3555,7 @@ _PyBuiltin_Init(PyInterpreterState *interp) SETBUILTIN("range", &PyRange_Type); SETBUILTIN("reversed", &PyReversed_Type); SETBUILTIN("set", &PySet_Type); + SETBUILTIN("sentinel", &PySentinel_Type); SETBUILTIN("slice", &PySlice_Type); SETBUILTIN("staticmethod", &PyStaticMethod_Type); SETBUILTIN("str", &PyUnicode_Type); diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index 74ca562824012b..8dabceafe9a57a 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -86,6 +86,7 @@ Objects/rangeobject.c - PyRange_Type - Objects/setobject.c - PyFrozenSet_Type - Objects/setobject.c - PySetIter_Type - Objects/setobject.c - PySet_Type - +Objects/sentinelobject.c - PySentinel_Type - Objects/sliceobject.c - PyEllipsis_Type - Objects/sliceobject.c - PySlice_Type - Objects/templateobject.c - _PyTemplateIter_Type - From ededfb79154567a5bdf2ab1bc5d3e8b3c04d6c0c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 17 Apr 2026 17:37:29 -0700 Subject: [PATCH 2/9] Update Doc/library/functions.rst Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com> --- Doc/library/functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index f7c454797b6e2e..28de82af91df30 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1837,7 +1837,7 @@ are always available. They are listed here in alphabetical order. MISSING Sentinel objects are truthy and compare equal only to themselves. They are - intended to be compared with the ``is`` operator. + intended to be compared with the :keyword:`is` operator. Shallow and deep copies of a sentinel object return the object itself. From 39f364cf9c3ffc185d76d0e33d4669f38e9fe220 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 17 Apr 2026 17:37:37 -0700 Subject: [PATCH 3/9] Update Doc/library/functions.rst Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com> --- Doc/library/functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 28de82af91df30..071df54ba162eb 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1845,7 +1845,7 @@ are always available. They are listed here in alphabetical order. identity when pickled and unpickled. Sentinels that are not importable by module and name are not picklable. - Sentinel objects support the ``|`` operator for use in type expressions:: + Sentinel objects support the :ref:`| ` operator for use in type expressions:: def next_value(default: int | MISSING = MISSING): ... From 0adf314a336d217ea891d6441e11d4890a1f8f7c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 16 Apr 2026 21:08:54 -0700 Subject: [PATCH 4/9] gc, name --- Lib/test/test_builtin.py | 17 ++++++++++++ Objects/sentinelobject.c | 56 ++++++++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index aad60389384055..03220de05d50e8 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -22,6 +22,7 @@ import typing import unittest import warnings +import weakref from contextlib import ExitStack from functools import partial from inspect import CO_COROUTINE @@ -1929,6 +1930,7 @@ def test_sentinel(self): with self.assertRaisesRegex(TypeError, "must be str"): sentinel(1) self.assertTrue(sentinel.__flags__ & support._TPFLAGS_IMMUTABLETYPE) + self.assertTrue(sentinel.__flags__ & support._TPFLAGS_HAVE_GC) self.assertFalse(sentinel.__flags__ & support._TPFLAGS_BASETYPE) with self.assertRaises(TypeError): class SubSentinel(sentinel): @@ -1957,6 +1959,21 @@ def test_sentinel_pickle(self): with self.assertRaises(pickle.PicklingError): pickle.dumps(missing, protocol=proto) + def test_sentinel_str_subclass_name_cycle(self): + class Name(str): + pass + + name = Name("MISSING") + missing = sentinel(name) + self.assertIs(missing.__name__, name) + self.assertTrue(gc.is_tracked(missing)) + + name.missing = missing + ref = weakref.ref(name) + del name, missing + support.gc_collect() + self.assertIsNone(ref()) + def test_sentinel_union(self): missing = sentinel("MISSING") diff --git a/Objects/sentinelobject.c b/Objects/sentinelobject.c index 820f1c4766836b..f3c818157777a1 100644 --- a/Objects/sentinelobject.c +++ b/Objects/sentinelobject.c @@ -3,6 +3,7 @@ #include "Python.h" #include "pycore_ceval.h" // _PyThreadState_GET() #include "pycore_interpframe.h" // _PyFrame_IsIncomplete() +#include "pycore_object.h" // _PyObject_GC_TRACK/UNTRACK() #include "pycore_sentinelobject.h" #include "pycore_stackref.h" // PyStackRef_AsPyObjectBorrow() #include "pycore_typeobject.h" // _Py_BaseObject_RichCompare() @@ -13,15 +14,15 @@ typedef struct { PyObject_HEAD PyObject *name; PyObject *module; -} sentineldesc; +} sentinelobject; -#define sentineldesc_CAST(op) \ - (assert(_PySentinel_Check(op)), _Py_CAST(sentineldesc *, (op))) +#define sentinelobject_CAST(op) \ + (assert(_PySentinel_Check(op)), _Py_CAST(sentinelobject *, (op))) /*[clinic input] -class sentinel "sentineldesc *" "&PySentinel_Type" +class sentinel "sentinelobject *" "&PySentinel_Type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=bcb13e08c7eacaee]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=8b88f8268d3b5775]*/ #include "clinic/sentinelobject.c.h" @@ -62,7 +63,7 @@ sentinel_new_impl(PyTypeObject *type, PyObject *name) return NULL; } - sentineldesc *self = PyObject_New(sentineldesc, type); + sentinelobject *self = PyObject_GC_New(sentinelobject, type); if (self == NULL) { Py_DECREF(name); Py_DECREF(module); @@ -70,22 +71,42 @@ sentinel_new_impl(PyTypeObject *type, PyObject *name) } self->name = name; self->module = module; + _PyObject_GC_TRACK(self); return (PyObject *)self; } static void sentinel_dealloc(PyObject *op) { - sentineldesc *self = sentineldesc_CAST(op); - Py_DECREF(self->name); - Py_DECREF(self->module); + _PyObject_GC_UNTRACK(op); + sentinelobject *self = sentinelobject_CAST(op); + Py_CLEAR(self->name); + Py_CLEAR(self->module); Py_TYPE(op)->tp_free(op); } +static int +sentinel_traverse(PyObject *op, visitproc visit, void *arg) +{ + sentinelobject *self = sentinelobject_CAST(op); + Py_VISIT(self->name); + Py_VISIT(self->module); + return 0; +} + +static int +sentinel_clear(PyObject *op) +{ + sentinelobject *self = sentinelobject_CAST(op); + Py_CLEAR(self->name); + Py_CLEAR(self->module); + return 0; +} + static PyObject * sentinel_repr(PyObject *op) { - sentineldesc *self = sentineldesc_CAST(op); + sentinelobject *self = sentinelobject_CAST(op); return Py_NewRef(self->name); } @@ -104,7 +125,7 @@ sentinel_deepcopy(PyObject *self, PyObject *Py_UNUSED(memo)) static PyObject * sentinel_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) { - sentineldesc *self = sentineldesc_CAST(op); + sentinelobject *self = sentinelobject_CAST(op); return Py_NewRef(self->name); } @@ -128,8 +149,8 @@ static PyMethodDef sentinel_methods[] = { }; static PyMemberDef sentinel_members[] = { - {"__name__", Py_T_OBJECT_EX, offsetof(sentineldesc, name), Py_READONLY}, - {"__module__", Py_T_OBJECT_EX, offsetof(sentineldesc, module), Py_READONLY}, + {"__name__", Py_T_OBJECT_EX, offsetof(sentinelobject, name), Py_READONLY}, + {"__module__", Py_T_OBJECT_EX, offsetof(sentinelobject, module), Py_READONLY}, {NULL} }; @@ -145,17 +166,20 @@ PyDoc_STRVAR(sentinel_doc, PyTypeObject PySentinel_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) .tp_name = "sentinel", - .tp_basicsize = sizeof(sentineldesc), + .tp_basicsize = sizeof(sentinelobject), .tp_dealloc = sentinel_dealloc, .tp_repr = sentinel_repr, .tp_as_number = &sentinel_as_number, .tp_hash = PyObject_GenericHash, .tp_getattro = PyObject_GenericGetAttr, - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE + | Py_TPFLAGS_HAVE_GC, .tp_doc = sentinel_doc, + .tp_traverse = sentinel_traverse, + .tp_clear = sentinel_clear, .tp_richcompare = _Py_BaseObject_RichCompare, .tp_methods = sentinel_methods, .tp_members = sentinel_members, .tp_new = sentinel_new, - .tp_free = PyObject_Free, + .tp_free = PyObject_GC_Del, }; From e4a106a70bca821bf59b19ace54e76983b589584 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 17 Apr 2026 19:23:53 -0700 Subject: [PATCH 5/9] C API --- Doc/c-api/concrete.rst | 1 + Doc/c-api/sentinel.rst | 31 +++++++++++++ Doc/data/refcounts.dat | 4 ++ Include/Python.h | 1 + Include/sentinelobject.h | 22 +++++++++ Lib/test/test_capi/test_object.py | 18 ++++++++ Makefile.pre.in | 1 + Modules/_testcapi/object.c | 19 ++++++++ Objects/sentinelobject.c | 73 +++++++++++++++++++++--------- PCbuild/pythoncore.vcxproj | 1 + PCbuild/pythoncore.vcxproj.filters | 3 ++ 11 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 Doc/c-api/sentinel.rst create mode 100644 Include/sentinelobject.h diff --git a/Doc/c-api/concrete.rst b/Doc/c-api/concrete.rst index 1746fe95eaaca9..3f38411a52de6b 100644 --- a/Doc/c-api/concrete.rst +++ b/Doc/c-api/concrete.rst @@ -112,6 +112,7 @@ Other Objects picklebuffer.rst weakref.rst capsule.rst + sentinel.rst frame.rst gen.rst coro.rst diff --git a/Doc/c-api/sentinel.rst b/Doc/c-api/sentinel.rst new file mode 100644 index 00000000000000..bf716696604c00 --- /dev/null +++ b/Doc/c-api/sentinel.rst @@ -0,0 +1,31 @@ +.. highlight:: c + +.. _sentinelobjects: + +Sentinel Objects +---------------- + +.. c:var:: PyTypeObject PySentinel_Type + + This instance of :c:type:`PyTypeObject` represents the Python + :class:`sentinel` type. This is the same object as :class:`builtins.sentinel`. + + .. versionadded:: next + +.. c:function:: int PySentinel_Check(PyObject *o) + + Return true if *o* is a :class:`sentinel` object. The :class:`sentinel` type + does not allow subclasses, so this check is exact. + + .. versionadded:: next + +.. c:function:: PyObject* PySentinel_New(const char *name, const char *module_name) + + Return a new :class:`sentinel` object with :attr:`~sentinel.__name__` set to + *name* and :attr:`~sentinel.__module__` set to *module_name*. + Return ``NULL`` with an exception set on failure. + + *module_name* should be the name of an importable module if the sentinel + should support pickling. + + .. versionadded:: next diff --git a/Doc/data/refcounts.dat b/Doc/data/refcounts.dat index 2a6e6b963134bb..663b79e45eec17 100644 --- a/Doc/data/refcounts.dat +++ b/Doc/data/refcounts.dat @@ -2037,6 +2037,10 @@ PySeqIter_Check:PyObject *:op:0: PySeqIter_New:PyObject*::+1: PySeqIter_New:PyObject*:seq:0: +PySentinel_New:PyObject*::+1: +PySentinel_New:const char*:name:: +PySentinel_New:const char*:module_name:: + PySequence_Check:int::: PySequence_Check:PyObject*:o:0: diff --git a/Include/Python.h b/Include/Python.h index e6e5cab67e2045..79252192a1dea1 100644 --- a/Include/Python.h +++ b/Include/Python.h @@ -117,6 +117,7 @@ __pragma(warning(disable: 4201)) #include "cpython/genobject.h" #include "descrobject.h" #include "genericaliasobject.h" +#include "sentinelobject.h" #include "warnings.h" #include "weakrefobject.h" #include "structseq.h" diff --git a/Include/sentinelobject.h b/Include/sentinelobject.h new file mode 100644 index 00000000000000..9d8577767b7485 --- /dev/null +++ b/Include/sentinelobject.h @@ -0,0 +1,22 @@ +/* Sentinel object interface */ + +#ifndef Py_SENTINELOBJECT_H +#define Py_SENTINELOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_LIMITED_API +PyAPI_DATA(PyTypeObject) PySentinel_Type; + +#define PySentinel_Check(op) Py_IS_TYPE((op), &PySentinel_Type) + +PyAPI_FUNC(PyObject *) PySentinel_New( + const char *name, + const char *module_name); +#endif + +#ifdef __cplusplus +} +#endif +#endif /* !Py_SENTINELOBJECT_H */ diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 67572ab1ba268d..252768cd281f2b 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -63,6 +63,24 @@ def test_get_constant_borrowed(self): self.check_get_constant(_testlimitedcapi.get_constant_borrowed) +class SentinelTest(unittest.TestCase): + + def test_pysentinel_new(self): + import pickle + + marker = _testcapi.pysentinel_new("CAPI_SENTINEL", __name__) + self.assertIs(type(marker), sentinel) + self.assertTrue(_testcapi.pysentinel_check(marker)) + self.assertFalse(_testcapi.pysentinel_check(object())) + self.assertEqual(marker.__name__, "CAPI_SENTINEL") + self.assertEqual(marker.__module__, __name__) + self.assertEqual(repr(marker), "CAPI_SENTINEL") + + globals()["CAPI_SENTINEL"] = marker + self.addCleanup(globals().pop, "CAPI_SENTINEL", None) + self.assertIs(pickle.loads(pickle.dumps(marker)), marker) + + class PrintTest(unittest.TestCase): def testPyObjectPrintObject(self): diff --git a/Makefile.pre.in b/Makefile.pre.in index 173cefc402ea9d..e5a75be972da7a 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -1239,6 +1239,7 @@ PYTHON_HEADERS= \ $(srcdir)/Include/rangeobject.h \ $(srcdir)/Include/refcount.h \ $(srcdir)/Include/setobject.h \ + $(srcdir)/Include/sentinelobject.h \ $(srcdir)/Include/sliceobject.h \ $(srcdir)/Include/structmember.h \ $(srcdir)/Include/structseq.h \ diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 9160005e00654f..0d8be128f7f36e 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -555,6 +555,23 @@ pyobject_dump(PyObject *self, PyObject *args) Py_RETURN_NONE; } +static PyObject * +pysentinel_new(PyObject *self, PyObject *args) +{ + const char *name; + const char *module_name; + if (!PyArg_ParseTuple(args, "ss", &name, &module_name)) { + return NULL; + } + return PySentinel_New(name, module_name); +} + +static PyObject * +pysentinel_check(PyObject *self, PyObject *obj) +{ + return PyBool_FromLong(PySentinel_Check(obj)); +} + static PyMethodDef test_methods[] = { {"call_pyobject_print", call_pyobject_print, METH_VARARGS}, @@ -585,6 +602,8 @@ static PyMethodDef test_methods[] = { {"clear_managed_dict", clear_managed_dict, METH_O, NULL}, {"is_uniquely_referenced", is_uniquely_referenced, METH_O}, {"pyobject_dump", pyobject_dump, METH_VARARGS}, + {"pysentinel_new", pysentinel_new, METH_VARARGS}, + {"pysentinel_check", pysentinel_check, METH_O}, {NULL}, }; diff --git a/Objects/sentinelobject.c b/Objects/sentinelobject.c index f3c818157777a1..e412b3ddf1b312 100644 --- a/Objects/sentinelobject.c +++ b/Objects/sentinelobject.c @@ -26,22 +26,42 @@ class sentinel "sentinelobject *" "&PySentinel_Type" #include "clinic/sentinelobject.c.h" + static PyObject * -sentinel_get_caller_module(void) +caller(void) { _PyInterpreterFrame *f = _PyThreadState_GET()->current_frame; - while (f && _PyFrame_IsIncomplete(f)) { - f = f->previous; + if (f == NULL) { + Py_RETURN_NONE; + } + if (f == NULL || PyStackRef_IsNull(f->f_funcobj)) { + Py_RETURN_NONE; } - if (f != NULL && !PyStackRef_IsNull(f->f_funcobj)) { - PyObject *module = PyFunction_GetModule( - PyStackRef_AsPyObjectBorrow(f->f_funcobj)); - if (module != NULL && module != Py_None) { - return Py_NewRef(module); - } + PyObject *r = PyFunction_GetModule(PyStackRef_AsPyObjectBorrow(f->f_funcobj)); + if (!r) { PyErr_Clear(); + Py_RETURN_NONE; } - return PyUnicode_FromString("builtins"); + return Py_NewRef(r); +} + +static PyObject * +sentinel_new_with_module(PyTypeObject *type, PyObject *name, PyObject *module) +{ + assert(PyUnicode_Check(name)); + + Py_INCREF(name); + Py_INCREF(module); + sentinelobject *self = PyObject_GC_New(sentinelobject, type); + if (self == NULL) { + Py_DECREF(name); + Py_DECREF(module); + return NULL; + } + self->name = name; + self->module = module; + _PyObject_GC_TRACK(self); + return (PyObject *)self; } /*[clinic input] @@ -56,23 +76,34 @@ static PyObject * sentinel_new_impl(PyTypeObject *type, PyObject *name) /*[clinic end generated code: output=4af55c6048bed30d input=3ab75704f39c119c]*/ { - Py_INCREF(name); - PyObject *module = sentinel_get_caller_module(); + PyObject *module = caller(); if (module == NULL) { - Py_DECREF(name); return NULL; } - sentinelobject *self = PyObject_GC_New(sentinelobject, type); - if (self == NULL) { - Py_DECREF(name); - Py_DECREF(module); + PyObject *self = sentinel_new_with_module(type, name, module); + Py_DECREF(module); + return self; +} + +PyObject * +PySentinel_New(const char *name, const char *module_name) +{ + PyObject *name_obj = PyUnicode_FromString(name); + if (name_obj == NULL) { return NULL; } - self->name = name; - self->module = module; - _PyObject_GC_TRACK(self); - return (PyObject *)self; + PyObject *module_obj = PyUnicode_FromString(module_name); + if (module_obj == NULL) { + Py_DECREF(name_obj); + return NULL; + } + + PyObject *sentinel = sentinel_new_with_module( + &PySentinel_Type, name_obj, module_obj); + Py_DECREF(module_obj); + Py_DECREF(name_obj); + return sentinel; } static void diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index a6f546795581c1..bdab43f8f75100 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -382,6 +382,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index 0d46ed39cb28db..a79743b39b46d7 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -222,6 +222,9 @@ Include + + Include + Include From e5b87890d26208445f31989553aadc3bacfbe41e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 17 Apr 2026 19:28:04 -0700 Subject: [PATCH 6/9] not needed --- Include/internal/pycore_sentinelobject.h | 19 ------------------- Objects/object.c | 1 - Objects/sentinelobject.c | 3 +-- Objects/unionobject.c | 3 +-- PCbuild/pythoncore.vcxproj | 1 - PCbuild/pythoncore.vcxproj.filters | 3 --- Python/bltinmodule.c | 1 - 7 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 Include/internal/pycore_sentinelobject.h diff --git a/Include/internal/pycore_sentinelobject.h b/Include/internal/pycore_sentinelobject.h deleted file mode 100644 index b8a7f03ab45dd1..00000000000000 --- a/Include/internal/pycore_sentinelobject.h +++ /dev/null @@ -1,19 +0,0 @@ -// Sentinel object interface. - -#ifndef Py_INTERNAL_SENTINELOBJECT_H -#define Py_INTERNAL_SENTINELOBJECT_H -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef Py_BUILD_CORE -# error "this header requires Py_BUILD_CORE define" -#endif - -extern PyTypeObject PySentinel_Type; -#define _PySentinel_Check(op) Py_IS_TYPE((op), &PySentinel_Type) - -#ifdef __cplusplus -} -#endif -#endif // !Py_INTERNAL_SENTINELOBJECT_H diff --git a/Objects/object.c b/Objects/object.c index 2d82fd05c817a8..86a522fcb0769b 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -28,7 +28,6 @@ #include "pycore_pyerrors.h" // _PyErr_Occurred() #include "pycore_pymem.h" // _PyMem_IsPtrFreed() #include "pycore_pystate.h" // _PyThreadState_GET() -#include "pycore_sentinelobject.h" // PySentinel_Type #include "pycore_symtable.h" // PySTEntry_Type #include "pycore_template.h" // _PyTemplate_Type _PyTemplateIter_Type #include "pycore_tuple.h" // _PyTuple_DebugMallocStats() diff --git a/Objects/sentinelobject.c b/Objects/sentinelobject.c index e412b3ddf1b312..5ef76e4566366a 100644 --- a/Objects/sentinelobject.c +++ b/Objects/sentinelobject.c @@ -4,7 +4,6 @@ #include "pycore_ceval.h" // _PyThreadState_GET() #include "pycore_interpframe.h" // _PyFrame_IsIncomplete() #include "pycore_object.h" // _PyObject_GC_TRACK/UNTRACK() -#include "pycore_sentinelobject.h" #include "pycore_stackref.h" // PyStackRef_AsPyObjectBorrow() #include "pycore_typeobject.h" // _Py_BaseObject_RichCompare() #include "pycore_unionobject.h" // _Py_union_from_tuple() @@ -17,7 +16,7 @@ typedef struct { } sentinelobject; #define sentinelobject_CAST(op) \ - (assert(_PySentinel_Check(op)), _Py_CAST(sentinelobject *, (op))) + (assert(PySentinel_Check(op)), _Py_CAST(sentinelobject *, (op))) /*[clinic input] class sentinel "sentinelobject *" "&PySentinel_Type" diff --git a/Objects/unionobject.c b/Objects/unionobject.c index 57de3217f9f1ea..0f6b1e44bc2402 100644 --- a/Objects/unionobject.c +++ b/Objects/unionobject.c @@ -1,7 +1,6 @@ // typing.Union -- used to represent e.g. Union[int, str], int | str #include "Python.h" #include "pycore_object.h" // _PyObject_GC_TRACK/UNTRACK -#include "pycore_sentinelobject.h" // _PySentinel_Check() #include "pycore_typevarobject.h" // _PyTypeAlias_Type, _Py_typing_type_repr #include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString #include "pycore_unionobject.h" @@ -246,7 +245,7 @@ is_unionable(PyObject *obj) { if (obj == Py_None || PyType_Check(obj) || - _PySentinel_Check(obj) || + PySentinel_Check(obj) || _PyGenericAlias_Check(obj) || _PyUnion_Check(obj) || Py_IS_TYPE(obj, &_PyTypeAlias_Type)) { diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index bdab43f8f75100..6d0ba6a8177417 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -311,7 +311,6 @@ - diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index a79743b39b46d7..81e64b1877d046 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -840,9 +840,6 @@ Include\internal - - Include\internal - Include\internal diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 51972b45bbedd6..51b864fd414629 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -16,7 +16,6 @@ #include "pycore_pyerrors.h" // _PyErr_NoMemory() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_pythonrun.h" // _Py_SourceAsString() -#include "pycore_sentinelobject.h" // PySentinel_Type #include "pycore_tuple.h" // _PyTuple_Recycle() #include "clinic/bltinmodule.c.h" From bdcb400a3b04028da9d624840c887ce2fed1985c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 17 Apr 2026 19:29:26 -0700 Subject: [PATCH 7/9] use it for NoExtraItems --- Lib/typing.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index e78fb8b71a996c..10a3f8117575ec 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3122,31 +3122,7 @@ def _namedtuple_mro_entries(bases): NamedTuple.__mro_entries__ = _namedtuple_mro_entries -class _SingletonMeta(type): - def __setattr__(cls, attr, value): - # TypeError is consistent with the behavior of NoneType - raise TypeError( - f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}" - ) - - -class _NoExtraItemsType(metaclass=_SingletonMeta): - """The type of the NoExtraItems singleton.""" - - __slots__ = () - - def __new__(cls): - return globals().get("NoExtraItems") or object.__new__(cls) - - def __repr__(self): - return 'typing.NoExtraItems' - - def __reduce__(self): - return 'NoExtraItems' - -NoExtraItems = _NoExtraItemsType() -del _NoExtraItemsType -del _SingletonMeta +NoExtraItems = sentinel("NoExtraItems") def _get_typeddict_qualifiers(annotation_type): From cc4545ccc466238920b43fa9995b43a3d3a3248f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 17 Apr 2026 19:39:13 -0700 Subject: [PATCH 8/9] tweaks --- Doc/library/functions.rst | 7 ++++++- .../2026-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) delete mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 071df54ba162eb..ca9f5d4c8575e1 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1845,11 +1845,16 @@ are always available. They are listed here in alphabetical order. identity when pickled and unpickled. Sentinels that are not importable by module and name are not picklable. - Sentinel objects support the :ref:`| ` operator for use in type expressions:: + Sentinels are conventionally assigned to a variable with a matching name. + Sentinels defined in this way can be used in :term:`type hints`:: + + MISSING = sentinel("MISSING") def next_value(default: int | MISSING = MISSING): ... + Sentinel objects support the :ref:`| ` operator for use in type expressions. + .. attribute:: __name__ The sentinel's name. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst deleted file mode 100644 index 9dab491cc7d077..00000000000000 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-17-00-00-00.gh-issue-00000.tnJ3Xy.rst +++ /dev/null @@ -1 +0,0 @@ -Add the :class:`sentinel` built-in class for creating unique sentinel values. From be582150d3ee7eb53521fc4ba21d1827fc58bd6c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 18 Apr 2026 05:54:23 -0700 Subject: [PATCH 9/9] fix warning? --- Doc/c-api/sentinel.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/sentinel.rst b/Doc/c-api/sentinel.rst index bf716696604c00..890706fe3f2272 100644 --- a/Doc/c-api/sentinel.rst +++ b/Doc/c-api/sentinel.rst @@ -8,7 +8,7 @@ Sentinel Objects .. c:var:: PyTypeObject PySentinel_Type This instance of :c:type:`PyTypeObject` represents the Python - :class:`sentinel` type. This is the same object as :class:`builtins.sentinel`. + :class:`sentinel` type. This is the same object as :class:`sentinel`. .. versionadded:: next