https://github.com/python/cpython/commit/29a92abb6052cdbbecf97f24f576e29da88d12fc
commit: 29a92abb6052cdbbecf97f24f576e29da88d12fc
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-04-27T19:28:30-07:00
summary:

gh-148829: Implement PEP 661 (#148831)

Co-authored-by: Victorien <[email protected]>
Co-authored-by: Pieter Eendebak <[email protected]>
Co-authored-by: Hugo van Kemenade <[email protected]>

files:
A Doc/c-api/sentinel.rst
A Include/sentinelobject.h
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-06-43-32.gh-issue-148829.GtIrYO.rst
A Objects/clinic/sentinelobject.c.h
A Objects/sentinelobject.c
M Doc/c-api/concrete.rst
M Doc/data/refcounts.dat
M Doc/library/functions.rst
M Doc/whatsnew/3.15.rst
M Include/Python.h
M Lib/test/pickletester.py
M Lib/test/test_builtin.py
M Lib/test/test_capi/test_object.py
M Lib/typing.py
M Makefile.pre.in
M Modules/_testcapi/object.c
M Objects/object.c
M Objects/unionobject.c
M PCbuild/_freeze_module.vcxproj
M PCbuild/_freeze_module.vcxproj.filters
M PCbuild/pythoncore.vcxproj
M PCbuild/pythoncore.vcxproj.filters
M Python/bltinmodule.c
M Tools/c-analyzer/cpython/globals-to-fix.tsv

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..710ded56e2a6db
--- /dev/null
+++ b/Doc/c-api/sentinel.rst
@@ -0,0 +1,35 @@
+.. 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:`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*.
+   *name* must not be ``NULL``. If *module_name* is ``NULL``, 
:attr:`~sentinel.__module__`
+   is set to ``None``.
+   Return ``NULL`` with an exception set on failure.
+
+   For pickling to work, *module_name* must be the name of an importable
+   module, and the sentinel must be accessible from that module under a
+   path matching *name*.  Pickle treats *name* as a global variable name
+   in *module_name* (see :meth:`object.__reduce__`).
+
+   .. 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/Doc/library/functions.rst b/Doc/library/functions.rst
index 119141d2e6daf3..aa99d198e436d5 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,61 @@ 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 :keyword:`is` operator.
+
+   Shallow and deep copies of a sentinel object return the object itself.
+
+   Sentinels are conventionally assigned to a variable with a matching name.
+   Sentinels defined in this way can be used in :term:`type hints <type 
hint>`::
+
+      MISSING = sentinel("MISSING")
+
+      def next_value(default: int | MISSING = MISSING):
+          ...
+
+   Sentinel objects support the :ref:`| <bitwise>` operator for use in type 
expressions.
+
+   :mod:`Pickling <pickle>` is supported for sentinel objects that are
+   placed in the global scope of a module under a name matching the sentinel's
+   name, and for sentinels placed in class scopes with a name matching the
+   :term:`qualified name` of the sentinel. Other sentinels, such as those
+   defined in a function scope, are not picklable. The identity of the 
sentinel is preserved
+   after pickling::
+
+      import pickle
+
+      PICKLABLE = sentinel("PICKLABLE")
+
+      assert pickle.loads(pickle.dumps(PICKLABLE)) is PICKLABLE
+
+      class Cls:
+          PICKLABLE = sentinel("Cls.PICKLABLE")
+
+      assert pickle.loads(pickle.dumps(Cls.PICKLABLE)) is Cls.PICKLABLE
+
+   Sentinel objects have the following attributes:
+
+   .. 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 405d388af487e8..0a96a970ba2329 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -69,6 +69,8 @@ Summary -- Release highlights
   <whatsnew315-lazy-imports>`
 * :pep:`814`: :ref:`Add frozendict built-in type
   <whatsnew315-frozendict>`
+* :pep:`661`: :ref:`Add sentinel built-in type
+  <whatsnew315-sentinel>`
 * :pep:`799`: :ref:`A dedicated profiling package for organizing Python
   profiling tools <whatsnew315-profiling-package>`
 * :pep:`799`: :ref:`Tachyon: High frequency statistical sampling profiler
@@ -247,6 +249,20 @@ 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.
+
+(PEP by Tal Einat; contributed by Jelle Zijlstra in :gh:`148829`.)
+
+
 .. _whatsnew315-profiling-package:
 
 :pep:`799`: A dedicated profiling package
diff --git a/Include/Python.h b/Include/Python.h
index 8b76195b320998..1272e2464f91d1 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/pickletester.py b/Lib/test/pickletester.py
index 6366f12257f3b5..c2018c9785b9b3 100644
--- a/Lib/test/pickletester.py
+++ b/Lib/test/pickletester.py
@@ -3244,6 +3244,7 @@ def test_builtin_types(self):
             'BuiltinImporter': (3, 3),
             'str': (3, 4),  # not interoperable with Python < 3.4
             'frozendict': (3, 15),
+            'sentinel': (3, 15),
         }
         for t in builtins.__dict__.values():
             if isinstance(t, type) and not issubclass(t, BaseException):
diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py
index 844656eb0e2c2e..e323742665234c 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
@@ -21,6 +22,7 @@
 import typing
 import unittest
 import warnings
+import weakref
 from contextlib import ExitStack
 from functools import partial
 from inspect import CO_COROUTINE
@@ -52,6 +54,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 +1909,98 @@ 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.assertTrue(sentinel.__flags__ & support._TPFLAGS_HAVE_GC)
+        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"
+        with self.assertRaises(AttributeError):
+            del missing.__name__
+        with self.assertRaises(AttributeError):
+            del missing.__module__
+
+    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_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")
+
+        self.assertIsInstance(missing | int, typing.Union)
+        self.assertEqual((missing | int).__args__, (missing, int))
+        self.assertIsInstance(int | missing, typing.Union)
+        self.assertEqual((int | missing).__args__, (int, missing))
+        self.assertIs(missing | missing, missing)
+        self.assertEqual(repr(int | missing), "int | MISSING")
+        self.assertIsInstance(missing | None, typing.Union)
+        self.assertEqual((missing | None).__args__, (missing, type(None)))
+        self.assertIsInstance(None | missing, typing.Union)
+        self.assertEqual((None | missing).__args__, (type(None), missing))
+        self.assertIsInstance(missing | list[int], typing.Union)
+        self.assertEqual((missing | list[int]).__args__, (missing, list[int]))
+        self.assertIsInstance(missing | (int | str), typing.Union)
+        self.assertEqual((missing | (int | str)).__args__, (missing, int, str))
+
+        with self.assertRaises(TypeError):
+            missing | 1
+        with self.assertRaises(TypeError):
+            1 | missing
+
     def test_round(self):
         self.assertEqual(round(0.0), 0.0)
         self.assertEqual(type(round(0.0)), int)
diff --git a/Lib/test/test_capi/test_object.py 
b/Lib/test/test_capi/test_object.py
index 67572ab1ba268d..635deaa73f7efa 100644
--- a/Lib/test/test_capi/test_object.py
+++ b/Lib/test/test_capi/test_object.py
@@ -1,5 +1,6 @@
 import enum
 import os
+import pickle
 import sys
 import textwrap
 import unittest
@@ -63,6 +64,27 @@ def test_get_constant_borrowed(self):
         self.check_get_constant(_testlimitedcapi.get_constant_borrowed)
 
 
+class SentinelTest(unittest.TestCase):
+
+    def test_pysentinel_new(self):
+        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")
+
+        no_module = _testcapi.pysentinel_new("NO_MODULE")
+        self.assertIs(type(no_module), sentinel)
+        self.assertEqual(no_module.__name__, "NO_MODULE")
+        self.assertIs(no_module.__module__, None)
+
+        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/Lib/typing.py b/Lib/typing.py
index 46e7122b6c91c5..e7563a53878da5 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -3150,31 +3150,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):
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 8b46db33a2ac18..2ce53c6a816212 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -560,6 +560,7 @@ OBJECT_OBJS=        \
                Objects/obmalloc.o \
                Objects/picklebufobject.o \
                Objects/rangeobject.o \
+               Objects/sentinelobject.o \
                Objects/setobject.o \
                Objects/sliceobject.o \
                Objects/structseq.o \
@@ -1240,6 +1241,7 @@ PYTHON_HEADERS= \
                $(srcdir)/Include/pytypedefs.h \
                $(srcdir)/Include/rangeobject.h \
                $(srcdir)/Include/refcount.h \
+               $(srcdir)/Include/sentinelobject.h \
                $(srcdir)/Include/setobject.h \
                $(srcdir)/Include/sliceobject.h \
                $(srcdir)/Include/structmember.h \
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-06-43-32.gh-issue-148829.GtIrYO.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-06-43-32.gh-issue-148829.GtIrYO.rst
new file mode 100644
index 00000000000000..3d9b4faa6ca443
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-21-06-43-32.gh-issue-148829.GtIrYO.rst
@@ -0,0 +1,2 @@
+Add :class:`sentinel`, implementing :pep:`661`. PEP by Tal Einat; patch by
+Jelle Zijlstra.
diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c
index 9160005e00654f..6e5c8dcbb725fa 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 = NULL;
+    if (!PyArg_ParseTuple(args, "s|s", &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/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 4fa20470601eb3..e6a764435bc292 100644
--- a/Objects/object.c
+++ b/Objects/object.c
@@ -2597,6 +2597,7 @@ static PyTypeObject* static_types[] = {
     &PyRange_Type,
     &PyReversed_Type,
     &PySTEntry_Type,
+    &PySentinel_Type,
     &PySeqIter_Type,
     &PySetIter_Type,
     &PySet_Type,
diff --git a/Objects/sentinelobject.c b/Objects/sentinelobject.c
new file mode 100644
index 00000000000000..e7e9f60e3edfbe
--- /dev/null
+++ b/Objects/sentinelobject.c
@@ -0,0 +1,196 @@
+/* Sentinel object implementation */
+
+#include "Python.h"
+#include "descrobject.h"          // PyMemberDef
+#include "pycore_ceval.h"         // _PyThreadState_GET()
+#include "pycore_interpframe.h"   // _PyFrame_IsIncomplete()
+#include "pycore_object.h"        // _PyObject_GC_TRACK/UNTRACK()
+#include "pycore_stackref.h"      // PyStackRef_AsPyObjectBorrow()
+#include "pycore_tuple.h"         // _PyTuple_FromPair
+#include "pycore_typeobject.h"    // _Py_BaseObject_RichCompare()
+#include "pycore_unionobject.h"   // _Py_union_type_or()
+
+typedef struct {
+    PyObject_HEAD
+    PyObject *name;
+    PyObject *module;
+} sentinelobject;
+
+#define sentinelobject_CAST(op) \
+    (assert(PySentinel_Check(op)), _Py_CAST(sentinelobject *, (op)))
+
+/*[clinic input]
+class sentinel "sentinelobject *" "&PySentinel_Type"
+[clinic start generated code]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=8b88f8268d3b5775]*/
+
+#include "clinic/sentinelobject.c.h"
+
+
+static PyObject *
+caller(void)
+{
+    _PyInterpreterFrame *f = _PyThreadState_GET()->current_frame;
+    if (f == NULL || PyStackRef_IsNull(f->f_funcobj)) {
+        assert(!PyErr_Occurred());
+        Py_RETURN_NONE;
+    }
+    PyFunctionObject *func = _PyFrame_GetFunction(f);
+    assert(PyFunction_Check(func));
+    PyObject *r = PyFunction_GetModule((PyObject *)func);
+    if (!r) {
+        assert(!PyErr_Occurred());
+        Py_RETURN_NONE;
+    }
+    return Py_NewRef(r);
+}
+
+static PyObject *
+sentinel_new_with_module(PyTypeObject *type, PyObject *name, PyObject *module)
+{
+    assert(PyUnicode_Check(name));
+
+    sentinelobject *self = PyObject_GC_New(sentinelobject, type);
+    if (self == NULL) {
+        return NULL;
+    }
+    self->name = Py_NewRef(name);
+    self->module = Py_NewRef(module);
+    _PyObject_GC_TRACK(self);
+    return (PyObject *)self;
+}
+
+/*[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]*/
+{
+    PyObject *module = caller();
+    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;
+    }
+    PyObject *module_obj = module_name == NULL
+        ? Py_None
+        : 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 int
+sentinel_clear(PyObject *op)
+{
+    sentinelobject *self = sentinelobject_CAST(op);
+    Py_CLEAR(self->name);
+    Py_CLEAR(self->module);
+    return 0;
+}
+
+static void
+sentinel_dealloc(PyObject *op)
+{
+    _PyObject_GC_UNTRACK(op);
+    (void)sentinel_clear(op);
+    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 PyObject *
+sentinel_repr(PyObject *op)
+{
+    sentinelobject *self = sentinelobject_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))
+{
+    sentinelobject *self = sentinelobject_CAST(op);
+    return Py_NewRef(self->name);
+}
+
+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(sentinelobject, name), Py_READONLY},
+    {"__module__", Py_T_OBJECT_EX, offsetof(sentinelobject, module), 
Py_READONLY},
+    {NULL}
+};
+
+static PyNumberMethods sentinel_as_number = {
+    .nb_or = _Py_union_type_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(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
+                | 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_GC_Del,
+};
diff --git a/Objects/unionobject.c b/Objects/unionobject.c
index d33d581f049c5b..0f6b1e44bc2402 100644
--- a/Objects/unionobject.c
+++ b/Objects/unionobject.c
@@ -245,6 +245,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..953973a2ad32df 100644
--- a/PCbuild/_freeze_module.vcxproj
+++ b/PCbuild/_freeze_module.vcxproj
@@ -158,6 +158,7 @@
     <ClCompile Include="..\Objects\odictobject.c" />
     <ClCompile Include="..\Objects\picklebufobject.c" />
     <ClCompile Include="..\Objects\rangeobject.c" />
+    <ClCompile Include="..\Objects\sentinelobject.c" />
     <ClCompile Include="..\Objects\setobject.c" />
     <ClCompile Include="..\Objects\sliceobject.c" />
     <ClCompile Include="..\Objects\structseq.c" />
diff --git a/PCbuild/_freeze_module.vcxproj.filters 
b/PCbuild/_freeze_module.vcxproj.filters
index 73861dbb0c9e7e..13db4d93f54518 100644
--- a/PCbuild/_freeze_module.vcxproj.filters
+++ b/PCbuild/_freeze_module.vcxproj.filters
@@ -400,6 +400,9 @@
     <ClCompile Include="..\Objects\rangeobject.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\Objects\sentinelobject.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\Objects\setobject.c">
       <Filter>Source Files</Filter>
     </ClCompile>
diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj
index 07305add81d055..fb9217fee8bd73 100644
--- a/PCbuild/pythoncore.vcxproj
+++ b/PCbuild/pythoncore.vcxproj
@@ -384,6 +384,7 @@
     <ClInclude Include="..\Include\pytypedefs.h" />
     <ClInclude Include="..\Include\rangeobject.h" />
     <ClInclude Include="..\Include\refcount.h" />
+    <ClInclude Include="..\Include\sentinelobject.h" />
     <ClInclude Include="..\Include\setobject.h" />
     <ClInclude Include="..\Include\sliceobject.h" />
     <ClInclude Include="..\Include\structmember.h" />
@@ -561,6 +562,7 @@
     <ClCompile Include="..\Objects\odictobject.c" />
     <ClCompile Include="..\Objects\picklebufobject.c" />
     <ClCompile Include="..\Objects\rangeobject.c" />
+    <ClCompile Include="..\Objects\sentinelobject.c" />
     <ClCompile Include="..\Objects\setobject.c" />
     <ClCompile Include="..\Objects\sliceobject.c" />
     <ClCompile Include="..\Objects\structseq.c" />
diff --git a/PCbuild/pythoncore.vcxproj.filters 
b/PCbuild/pythoncore.vcxproj.filters
index 629f063861de9a..1e1d085cd75511 100644
--- a/PCbuild/pythoncore.vcxproj.filters
+++ b/PCbuild/pythoncore.vcxproj.filters
@@ -222,6 +222,9 @@
     <ClInclude Include="..\Include\runtime_structs.h">
       <Filter>Include</Filter>
     </ClInclude>
+    <ClInclude Include="..\Include\sentinelobject.h">
+      <Filter>Include</Filter>
+    </ClInclude>
     <ClInclude Include="..\Include\setobject.h">
       <Filter>Include</Filter>
     </ClInclude>
@@ -1274,6 +1277,9 @@
     <ClCompile Include="..\Objects\rangeobject.c">
       <Filter>Objects</Filter>
     </ClCompile>
+    <ClCompile Include="..\Objects\sentinelobject.c">
+      <Filter>Objects</Filter>
+    </ClCompile>
     <ClCompile Include="..\Objects\setobject.c">
       <Filter>Objects</Filter>
     </ClCompile>
diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c
index 16413d784cc87c..35b30a243318cc 100644
--- a/Python/bltinmodule.c
+++ b/Python/bltinmodule.c
@@ -3555,6 +3555,7 @@ _PyBuiltin_Init(PyInterpreterState *interp)
     SETBUILTIN("object",                &PyBaseObject_Type);
     SETBUILTIN("range",                 &PyRange_Type);
     SETBUILTIN("reversed",              &PyReversed_Type);
+    SETBUILTIN("sentinel",              &PySentinel_Type);
     SETBUILTIN("set",                   &PySet_Type);
     SETBUILTIN("slice",                 &PySlice_Type);
     SETBUILTIN("staticmethod",          &PyStaticMethod_Type);
diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv 
b/Tools/c-analyzer/cpython/globals-to-fix.tsv
index 74ca562824012b..db575d870be5c5 100644
--- a/Tools/c-analyzer/cpython/globals-to-fix.tsv
+++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv
@@ -83,6 +83,7 @@ Objects/picklebufobject.c     -       PyPickleBuffer_Type     
-
 Objects/rangeobject.c  -       PyLongRangeIter_Type    -
 Objects/rangeobject.c  -       PyRangeIter_Type        -
 Objects/rangeobject.c  -       PyRange_Type    -
+Objects/sentinelobject.c       -       PySentinel_Type -
 Objects/setobject.c    -       PyFrozenSet_Type        -
 Objects/setobject.c    -       PySetIter_Type  -
 Objects/setobject.c    -       PySet_Type      -

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to