https://github.com/python/cpython/commit/696cdfc0a25aa4f1ba200464bd710ff54a261a78
commit: 696cdfc0a25aa4f1ba200464bd710ff54a261a78
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-02-17T10:54:41+01:00
summary:

gh-141510, PEP 814: Add built-in frozendict type (#144757)

Add TYPE_FROZENDICT to the marshal module.

Add C API functions:

* PyAnyDict_Check()
* PyAnyDict_CheckExact()
* PyFrozenDict_Check()
* PyFrozenDict_CheckExact()
* PyFrozenDict_New()

Add PyFrozenDict_Type C type.

Co-authored-by: Hugo van Kemenade <[email protected]>
Co-authored-by: Adam Johnson <[email protected]>
Co-authored-by: Benedikt Johannes <[email protected]>

files:
A Misc/NEWS.d/next/C_API/2026-02-12-19-03-31.gh-issue-141510.U_1tjz.rst
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-02-12-19-01-13.gh-issue-141510.KlKjZg.rst
M Doc/c-api/dict.rst
M Doc/library/stdtypes.rst
M Doc/whatsnew/3.15.rst
M Include/cpython/dictobject.h
M Include/internal/pycore_dict.h
M Include/internal/pycore_typeobject.h
M Lib/_collections_abc.py
M Lib/test/mapping_tests.py
M Lib/test/test_dict.py
M Lib/test/test_doctest/test_doctest.py
M Lib/test/test_inspect/test_inspect.py
M Objects/dictobject.c
M Objects/object.c
M Python/bltinmodule.c
M Python/marshal.c
M Tools/c-analyzer/cpython/ignored.tsv

diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst
index 9c4428ced41b5a..736f282e7bd47a 100644
--- a/Doc/c-api/dict.rst
+++ b/Doc/c-api/dict.rst
@@ -2,7 +2,7 @@
 
 .. _dictobjects:
 
-Dictionary Objects
+Dictionary objects
 ------------------
 
 .. index:: pair: object; dictionary
@@ -444,7 +444,7 @@ Dictionary Objects
    .. versionadded:: 3.12
 
 
-Dictionary View Objects
+Dictionary view objects
 ^^^^^^^^^^^^^^^^^^^^^^^
 
 .. c:function:: int PyDictViewSet_Check(PyObject *op)
@@ -490,7 +490,58 @@ Dictionary View Objects
    always succeeds.
 
 
-Ordered Dictionaries
+Frozen dictionary objects
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. versionadded:: next
+
+
+.. c:var:: PyTypeObject PyFrozenDict_Type
+
+   This instance of :c:type:`PyTypeObject` represents the Python frozen
+   dictionary type.
+   This is the same object as :class:`frozendict` in the Python layer.
+
+
+.. c:function:: int PyAnyDict_Check(PyObject *p)
+
+   Return true if *p* is a :class:`dict` object, a :class:`frozendict` object,
+   or an instance of a subtype of the :class:`!dict` or :class:`!frozendict`
+   type.
+   This function always succeeds.
+
+
+.. c:function:: int PyAnyDict_CheckExact(PyObject *p)
+
+   Return true if *p* is a :class:`dict` object or a :class:`frozendict` 
object,
+   but not an instance of a subtype of the :class:`!dict` or
+   :class:`!frozendict` type.
+   This function always succeeds.
+
+
+.. c:function:: int PyFrozenDict_Check(PyObject *p)
+
+   Return true if *p* is a :class:`frozendict` object or an instance of a
+   subtype of the :class:`!frozendict` type.
+   This function always succeeds.
+
+
+.. c:function:: int PyFrozenDict_CheckExact(PyObject *p)
+
+   Return true if *p* is a :class:`frozendict` object, but not an instance of a
+   subtype of the :class:`!frozendict` type.
+   This function always succeeds.
+
+
+.. c:function:: PyObject* PyFrozenDict_New(PyObject *iterable)
+
+   Return a new :class:`frozendict` from an iterable, or ``NULL`` on failure
+   with an exception set.
+
+   Create an empty dictionary if *iterable* is ``NULL``.
+
+
+Ordered dictionaries
 ^^^^^^^^^^^^^^^^^^^^
 
 Python's C API provides interface for :class:`collections.OrderedDict` from C.
diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst
index d4540e0b819871..6f798f02e17899 100644
--- a/Doc/library/stdtypes.rst
+++ b/Doc/library/stdtypes.rst
@@ -5305,8 +5305,8 @@ frozenset, a temporary one is created from *elem*.
 
 .. _typesmapping:
 
-Mapping Types --- :class:`dict`
-===============================
+Mapping types --- :class:`!dict`, :class:`!frozendict`
+======================================================
 
 .. index::
    pair: object; mapping
@@ -5317,8 +5317,9 @@ Mapping Types --- :class:`dict`
    pair: built-in function; len
 
 A :term:`mapping` object maps :term:`hashable` values to arbitrary objects.
-Mappings are mutable objects.  There is currently only one standard mapping
-type, the :dfn:`dictionary`.  (For other containers see the built-in
+There are currently two standard mapping types, the :dfn:`dictionary` and
+:class:`frozendict`.
+(For other containers see the built-in
 :class:`list`, :class:`set`, and :class:`tuple` classes, and the
 :mod:`collections` module.)
 
@@ -5588,10 +5589,9 @@ can be used interchangeably to index the same dictionary 
entry.
       Dictionaries are now reversible.
 
 
-.. seealso::
-   :class:`types.MappingProxyType` can be used to create a read-only view
-   of a :class:`dict`.
-
+   .. seealso::
+      :class:`types.MappingProxyType` can be used to create a read-only view
+      of a :class:`dict`.
 
 .. _thread-safety-dict:
 
@@ -5839,6 +5839,41 @@ An example of dictionary view usage::
    500
 
 
+Frozen dictionaries
+-------------------
+
+.. class:: frozendict(**kwargs)
+           frozendict(mapping, /, **kwargs)
+           frozendict(iterable, /, **kwargs)
+
+   Return a new frozen dictionary initialized from an optional positional
+   argument and a possibly empty set of keyword arguments.
+
+   A :class:`!frozendict` has a similar API to the :class:`dict` API, with the
+   following differences:
+
+   * :class:`!dict` has more methods than :class:`!frozendict`:
+
+      * :meth:`!__delitem__`
+      * :meth:`!__setitem__`
+      * :meth:`~dict.clear`
+      * :meth:`~dict.pop`
+      * :meth:`~dict.popitem`
+      * :meth:`~dict.setdefault`
+      * :meth:`~dict.update`
+
+   * A :class:`!frozendict` can be hashed with ``hash(frozendict)`` if all 
keys and
+     values can be hashed.
+
+   * ``frozendict |= other`` does not modify the :class:`!frozendict` in-place 
but
+     creates a new frozen dictionary.
+
+   :class:`!frozendict` is not a :class:`!dict` subclass but inherits directly
+   from ``object``.
+
+   .. versionadded:: next
+
+
 .. _typecontextmanager:
 
 Context Manager Types
@@ -6062,6 +6097,7 @@ list is non-exhaustive.
 * :class:`list`
 * :class:`dict`
 * :class:`set`
+* :class:`frozendict`
 * :class:`frozenset`
 * :class:`type`
 * :class:`asyncio.Future`
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 5dca1545b144ef..e5714765208ff6 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -67,6 +67,8 @@ Summary -- Release highlights
 
 * :pep:`810`: :ref:`Explicit lazy imports for faster startup times
   <whatsnew315-pep810>`
+* :pep:`814`: :ref:`Add frozendict built-in type
+  <whatsnew315-frozendict>`
 * :pep:`799`: :ref:`A dedicated profiling package for organizing Python
   profiling tools <whatsnew315-profiling-package>`
 * :pep:`799`: :ref:`Tachyon: High frequency statistical sampling profiler
@@ -180,6 +182,21 @@ raise :exc:`SyntaxError`).
 
 (Contributed by Pablo Galindo Salgado and Dino Viehland in :gh:`142349`.)
 
+
+.. _whatsnew315-frozendict:
+
+:pep:`814`: Add frozendict built-in type
+----------------------------------------
+
+A new public immutable type :class:`frozendict` is added to the :mod:`builtins`
+module. It is not a ``dict`` subclass but inherits directly from ``object``.
+
+A ``frozendict`` can be hashed with ``hash(frozendict)`` if all keys and values
+can be hashed.
+
+.. seealso:: :pep:`814` for the full specification and rationale.
+
+
 .. _whatsnew315-profiling-package:
 
 :pep:`799`: A dedicated profiling package
@@ -1525,6 +1542,16 @@ C API changes
 New features
 ------------
 
+* Add the following functions for the new :class:`frozendict` type:
+
+  * :c:func:`PyAnyDict_Check`
+  * :c:func:`PyAnyDict_CheckExact`
+  * :c:func:`PyFrozenDict_Check`
+  * :c:func:`PyFrozenDict_CheckExact`
+  * :c:func:`PyFrozenDict_New`
+
+  (Contributed by Victor Stinner in :gh:`141510`.)
+
 * Add :c:func:`PySys_GetAttr`, :c:func:`PySys_GetAttrString`,
   :c:func:`PySys_GetOptionalAttr`, and :c:func:`PySys_GetOptionalAttrString`
   functions as replacements for :c:func:`PySys_GetObject`.
diff --git a/Include/cpython/dictobject.h b/Include/cpython/dictobject.h
index 5f2f7b6d4f56bd..5e7811416aba63 100644
--- a/Include/cpython/dictobject.h
+++ b/Include/cpython/dictobject.h
@@ -32,6 +32,16 @@ typedef struct {
     PyDictValues *ma_values;
 } PyDictObject;
 
+// frozendict
+PyAPI_DATA(PyTypeObject) PyFrozenDict_Type;
+#define PyFrozenDict_Check(op) PyObject_TypeCheck((op), &PyFrozenDict_Type)
+#define PyFrozenDict_CheckExact(op) Py_IS_TYPE((op), &PyFrozenDict_Type)
+
+#define PyAnyDict_CheckExact(ob) \
+    (PyDict_CheckExact(ob) || PyFrozenDict_CheckExact(ob))
+#define PyAnyDict_Check(ob) \
+    (PyDict_Check(ob) || PyFrozenDict_Check(ob))
+
 PyAPI_FUNC(PyObject *) _PyDict_GetItem_KnownHash(PyObject *mp, PyObject *key,
                                                  Py_hash_t hash);
 // PyDict_GetItemStringRef() can be used instead
@@ -42,7 +52,7 @@ PyAPI_FUNC(PyObject *) PyDict_SetDefault(
 /* Get the number of items of a dictionary. */
 static inline Py_ssize_t PyDict_GET_SIZE(PyObject *op) {
     PyDictObject *mp;
-    assert(PyDict_Check(op));
+    assert(PyAnyDict_Check(op));
     mp = _Py_CAST(PyDictObject*, op);
 #ifdef Py_GIL_DISABLED
     return _Py_atomic_load_ssize_relaxed(&mp->ma_used);
@@ -93,3 +103,6 @@ PyAPI_FUNC(int) PyDict_ClearWatcher(int watcher_id);
 // Mark given dictionary as "watched" (callback will be called if it is 
modified)
 PyAPI_FUNC(int) PyDict_Watch(int watcher_id, PyObject* dict);
 PyAPI_FUNC(int) PyDict_Unwatch(int watcher_id, PyObject* dict);
+
+// Create a frozendict. Create an empty dictionary if iterable is NULL.
+PyAPI_FUNC(PyObject*) PyFrozenDict_New(PyObject *iterable);
diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h
index 379bf6a81784b0..59e88be6aeec12 100644
--- a/Include/internal/pycore_dict.h
+++ b/Include/internal/pycore_dict.h
@@ -408,6 +408,15 @@ _Py_DECREF_BUILTINS(PyObject *op)
 }
 #endif
 
+/* frozendict */
+typedef struct {
+    PyDictObject ob_base;
+    Py_hash_t ma_hash;
+} PyFrozenDictObject;
+
+#define _PyFrozenDictObject_CAST(op) \
+    (assert(PyFrozenDict_Check(op)), _Py_CAST(PyFrozenDictObject*, (op)))
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/Include/internal/pycore_typeobject.h 
b/Include/internal/pycore_typeobject.h
index dfd355d5012066..8af317d54c0bda 100644
--- a/Include/internal/pycore_typeobject.h
+++ b/Include/internal/pycore_typeobject.h
@@ -26,6 +26,7 @@ extern "C" {
 #define _Py_TYPE_VERSION_BYTEARRAY 9
 #define _Py_TYPE_VERSION_BYTES 10
 #define _Py_TYPE_VERSION_COMPLEX 11
+#define _Py_TYPE_VERSION_FROZENDICT 12
 
 #define _Py_TYPE_VERSION_NEXT 16
 
diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py
index 60b471317ce97c..23cc6d8faae2da 100644
--- a/Lib/_collections_abc.py
+++ b/Lib/_collections_abc.py
@@ -823,6 +823,7 @@ def __eq__(self, other):
 
     __reversed__ = None
 
+Mapping.register(frozendict)
 Mapping.register(mappingproxy)
 Mapping.register(framelocalsproxy)
 
diff --git a/Lib/test/mapping_tests.py b/Lib/test/mapping_tests.py
index 20306e1526d7b8..9624072e69adfc 100644
--- a/Lib/test/mapping_tests.py
+++ b/Lib/test/mapping_tests.py
@@ -4,7 +4,7 @@
 from test import support
 
 
-class BasicTestMappingProtocol(unittest.TestCase):
+class BasicTestImmutableMappingProtocol(unittest.TestCase):
     # This base class can be used to check that an object conforms to the
     # mapping protocol
 
@@ -20,12 +20,9 @@ def _empty_mapping(self):
         """Return an empty mapping object"""
         return self.type2test()
     def _full_mapping(self, data):
-        """Return a mapping object with the value contained in data
+        """Return a mapping object with the values contained in data
         dictionary"""
-        x = self._empty_mapping()
-        for key, value in data.items():
-            x[key] = value
-        return x
+        return self.type2test(data)
 
     def __init__(self, *args, **kw):
         unittest.TestCase.__init__(self, *args, **kw)
@@ -88,6 +85,72 @@ def check_iterandlist(iter, lst, ref):
         self.assertEqual(d.get(knownkey, knownvalue), knownvalue)
         self.assertNotIn(knownkey, d)
 
+    def test_constructor(self):
+        self.assertEqual(self._empty_mapping(), self._empty_mapping())
+
+    def test_bool(self):
+        self.assertTrue(not self._empty_mapping())
+        self.assertTrue(self.reference)
+        self.assertFalse(bool(self._empty_mapping()))
+        self.assertTrue(bool(self.reference))
+
+    def test_keys(self):
+        d = self._empty_mapping()
+        self.assertEqual(list(d.keys()), [])
+        d = self.reference
+        self.assertIn(list(self.inmapping.keys())[0], d.keys())
+        self.assertNotIn(list(self.other.keys())[0], d.keys())
+        self.assertRaises(TypeError, d.keys, None)
+
+    def test_values(self):
+        d = self._empty_mapping()
+        self.assertEqual(list(d.values()), [])
+
+        self.assertRaises(TypeError, d.values, None)
+
+    def test_items(self):
+        d = self._empty_mapping()
+        self.assertEqual(list(d.items()), [])
+
+        self.assertRaises(TypeError, d.items, None)
+
+    def test_len(self):
+        d = self._empty_mapping()
+        self.assertEqual(len(d), 0)
+
+    def test_getitem(self):
+        d = self.reference
+        self.assertEqual(d[list(self.inmapping.keys())[0]],
+                         list(self.inmapping.values())[0])
+
+        self.assertRaises(TypeError, d.__getitem__)
+
+    # no test_fromkeys or test_copy as both os.environ and selves don't 
support it
+
+    def test_get(self):
+        d = self._empty_mapping()
+        self.assertIsNone(d.get(list(self.other.keys())[0]))
+        self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
+        d = self.reference
+        self.assertIsNone(d.get(list(self.other.keys())[0]))
+        self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
+        self.assertEqual(d.get(list(self.inmapping.keys())[0]),
+                         list(self.inmapping.values())[0])
+        self.assertEqual(d.get(list(self.inmapping.keys())[0], 3),
+                         list(self.inmapping.values())[0])
+        self.assertRaises(TypeError, d.get)
+        self.assertRaises(TypeError, d.get, None, None, None)
+
+
+class BasicTestMappingProtocol(BasicTestImmutableMappingProtocol):
+    def _full_mapping(self, data):
+        """Return a mapping object with the values contained in data
+        dictionary"""
+        x = self._empty_mapping()
+        for key, value in data.items():
+            x[key] = value
+        return x
+
     def test_write(self):
         # Test for write operations on mapping
         p = self._empty_mapping()
@@ -130,46 +193,6 @@ def test_write(self):
         p=self._empty_mapping()
         self.assertRaises(KeyError, p.popitem)
 
-    def test_constructor(self):
-        self.assertEqual(self._empty_mapping(), self._empty_mapping())
-
-    def test_bool(self):
-        self.assertTrue(not self._empty_mapping())
-        self.assertTrue(self.reference)
-        self.assertTrue(bool(self._empty_mapping()) is False)
-        self.assertTrue(bool(self.reference) is True)
-
-    def test_keys(self):
-        d = self._empty_mapping()
-        self.assertEqual(list(d.keys()), [])
-        d = self.reference
-        self.assertIn(list(self.inmapping.keys())[0], d.keys())
-        self.assertNotIn(list(self.other.keys())[0], d.keys())
-        self.assertRaises(TypeError, d.keys, None)
-
-    def test_values(self):
-        d = self._empty_mapping()
-        self.assertEqual(list(d.values()), [])
-
-        self.assertRaises(TypeError, d.values, None)
-
-    def test_items(self):
-        d = self._empty_mapping()
-        self.assertEqual(list(d.items()), [])
-
-        self.assertRaises(TypeError, d.items, None)
-
-    def test_len(self):
-        d = self._empty_mapping()
-        self.assertEqual(len(d), 0)
-
-    def test_getitem(self):
-        d = self.reference
-        self.assertEqual(d[list(self.inmapping.keys())[0]],
-                         list(self.inmapping.values())[0])
-
-        self.assertRaises(TypeError, d.__getitem__)
-
     def test_update(self):
         # mapping argument
         d = self._empty_mapping()
@@ -265,22 +288,6 @@ def __next__(self):
 
         self.assertRaises(ValueError, d.update, [(1, 2, 3)])
 
-    # no test_fromkeys or test_copy as both os.environ and selves don't 
support it
-
-    def test_get(self):
-        d = self._empty_mapping()
-        self.assertTrue(d.get(list(self.other.keys())[0]) is None)
-        self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
-        d = self.reference
-        self.assertTrue(d.get(list(self.other.keys())[0]) is None)
-        self.assertEqual(d.get(list(self.other.keys())[0], 3), 3)
-        self.assertEqual(d.get(list(self.inmapping.keys())[0]),
-                         list(self.inmapping.values())[0])
-        self.assertEqual(d.get(list(self.inmapping.keys())[0], 3),
-                         list(self.inmapping.values())[0])
-        self.assertRaises(TypeError, d.get)
-        self.assertRaises(TypeError, d.get, None, None, None)
-
     def test_setdefault(self):
         d = self._empty_mapping()
         self.assertRaises(TypeError, d.setdefault)
diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py
index 6583c0f2aefb2b..1db3559a012fd3 100644
--- a/Lib/test/test_dict.py
+++ b/Lib/test/test_dict.py
@@ -1723,6 +1723,58 @@ class Dict(dict):
 class SubclassMappingTests(mapping_tests.BasicTestMappingProtocol):
     type2test = Dict
 
+class FrozenDictMappingTests(mapping_tests.BasicTestImmutableMappingProtocol):
+    type2test = frozendict
+
+
+class FrozenDict(frozendict):
+    pass
+
+
+class FrozenDictTests(unittest.TestCase):
+    def test_copy(self):
+        d = frozendict(x=1, y=2)
+        d2 = d.copy()
+        self.assertIs(d2, d)
+
+        d = FrozenDict(x=1, y=2)
+        d2 = d.copy()
+        self.assertIsNot(d2, d)
+        self.assertEqual(d2, frozendict(x=1, y=2))
+        self.assertEqual(type(d2), frozendict)
+
+    def test_merge(self):
+        # test "a | b" operator
+        self.assertEqual(frozendict(x=1) | frozendict(y=2),
+                         frozendict({'x': 1, 'y': 2}))
+        self.assertEqual(frozendict(x=1) | dict(y=2),
+                         frozendict({'x': 1, 'y': 2}))
+        self.assertEqual(frozendict(x=1, y=2) | frozendict(y=5),
+                         frozendict({'x': 1, 'y': 5}))
+        fd = frozendict(x=1, y=2)
+        self.assertIs(fd | frozendict(), fd)
+        self.assertIs(fd | {}, fd)
+        self.assertIs(frozendict() | fd, fd)
+
+    def test_update(self):
+        # test "a |= b" operator
+        d = frozendict(x=1)
+        copy = d
+        self.assertIs(copy, d)
+        d |= frozendict(y=2)
+        self.assertIsNot(copy, d)
+        self.assertEqual(d, frozendict({'x': 1, 'y': 2}))
+        self.assertEqual(copy, frozendict({'x': 1}))
+
+    def test_repr(self):
+        d = frozendict(x=1, y=2)
+        self.assertEqual(repr(d), "frozendict({'x': 1, 'y': 2})")
+
+        class MyFrozenDict(frozendict):
+            pass
+        d = MyFrozenDict(x=1, y=2)
+        self.assertEqual(repr(d), "MyFrozenDict({'x': 1, 'y': 2})")
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/Lib/test/test_doctest/test_doctest.py 
b/Lib/test/test_doctest/test_doctest.py
index 241d09db1fa70e..b125693ab0891c 100644
--- a/Lib/test/test_doctest/test_doctest.py
+++ b/Lib/test/test_doctest/test_doctest.py
@@ -742,7 +742,7 @@ def non_Python_modules(): r"""
 
     >>> import builtins
     >>> tests = doctest.DocTestFinder().find(builtins)
-    >>> 750 < len(tests) < 800 # approximate number of objects with docstrings
+    >>> 750 < len(tests) < 850 # approximate number of objects with docstrings
     True
     >>> real_tests = [t for t in tests if len(t.examples) > 0]
     >>> len(real_tests) # objects that actually have doctests
diff --git a/Lib/test/test_inspect/test_inspect.py 
b/Lib/test/test_inspect/test_inspect.py
index 32bb47de04b113..00cc5aab32c273 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -6127,7 +6127,8 @@ def _test_builtin_methods_have_signatures(self, cls, 
no_signature, unsupported_s
                 self.assertRaises(ValueError, inspect.signature, getattr(cls, 
name))
 
     def test_builtins_have_signatures(self):
-        no_signature = {'type', 'super', 'bytearray', 'bytes', 'dict', 'int', 
'str'}
+        no_signature = {'type', 'super', 'bytearray', 'bytes',
+                        'dict', 'frozendict', 'int', 'str'}
         # These need PEP 457 groups
         needs_groups = {"range", "slice", "dir", "getattr",
                         "next", "iter", "vars"}
diff --git 
a/Misc/NEWS.d/next/C_API/2026-02-12-19-03-31.gh-issue-141510.U_1tjz.rst 
b/Misc/NEWS.d/next/C_API/2026-02-12-19-03-31.gh-issue-141510.U_1tjz.rst
new file mode 100644
index 00000000000000..57a25fe045f04c
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2026-02-12-19-03-31.gh-issue-141510.U_1tjz.rst
@@ -0,0 +1,9 @@
+Add the following functions for the new :class:`frozendict` type:
+
+* :c:func:`PyAnyDict_Check`
+* :c:func:`PyAnyDict_CheckExact`
+* :c:func:`PyFrozenDict_Check`
+* :c:func:`PyFrozenDict_CheckExact`
+* :c:func:`PyFrozenDict_New`
+
+Patch by Victor Stinner.
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-12-19-01-13.gh-issue-141510.KlKjZg.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-12-19-01-13.gh-issue-141510.KlKjZg.rst
new file mode 100644
index 00000000000000..4596e273fc6118
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-12-19-01-13.gh-issue-141510.KlKjZg.rst
@@ -0,0 +1 @@
+Add built-in :class:`frozendict` type. Patch by Victor Stinner.
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index ae7bf61767dc3b..46b0148cf59ab5 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -135,6 +135,10 @@ As a consequence of this, split keys have a maximum size 
of 16.
 #include "stringlib/eq.h"                // unicode_eq()
 #include <stdbool.h>
 
+// Forward declarations
+static PyObject* frozendict_new(PyTypeObject *type, PyObject *args,
+                                PyObject *kwds);
+
 
 /*[clinic input]
 class dict "PyDictObject *" "&PyDict_Type"
@@ -278,6 +282,11 @@ load_keys_nentries(PyDictObject *mp)
 
 #endif
 
+#define _PyAnyDict_CAST(op) \
+    (assert(PyAnyDict_Check(op)), _Py_CAST(PyDictObject*, op))
+
+#define GET_USED(ep) FT_ATOMIC_LOAD_SSIZE_RELAXED((ep)->ma_used)
+
 #define STORE_KEY(ep, key) FT_ATOMIC_STORE_PTR_RELEASE((ep)->me_key, key)
 #define STORE_VALUE(ep, value) FT_ATOMIC_STORE_PTR_RELEASE((ep)->me_value, 
value)
 #define STORE_SPLIT_VALUE(mp, idx, value) 
FT_ATOMIC_STORE_PTR_RELEASE(mp->ma_values->values[idx], value)
@@ -654,7 +663,7 @@ _PyDict_CheckConsistency(PyObject *op, int check_content)
     do { if (!(expr)) { _PyObject_ASSERT_FAILED_MSG(op, Py_STRINGIFY(expr)); } 
} while (0)
 
     assert(op != NULL);
-    CHECK(PyDict_Check(op));
+    CHECK(PyAnyDict_Check(op));
     PyDictObject *mp = (PyDictObject *)op;
 
     PyDictKeysObject *keys = mp->ma_keys;
@@ -909,7 +918,7 @@ new_dict_with_shared_keys(PyDictKeysObject *keys)
 static PyDictKeysObject *
 clone_combined_dict_keys(PyDictObject *orig)
 {
-    assert(PyDict_Check(orig));
+    assert(PyAnyDict_Check(orig));
     assert(Py_TYPE(orig)->tp_iter == dict_iter);
     assert(orig->ma_values == NULL);
     assert(orig->ma_keys != Py_EMPTY_KEYS);
@@ -2293,7 +2302,7 @@ _PyDict_FromItems(PyObject *const *keys, Py_ssize_t 
keys_offset,
 static PyObject *
 dict_getitem(PyObject *op, PyObject *key, const char *warnmsg)
 {
-    if (!PyDict_Check(op)) {
+    if (!PyAnyDict_Check(op)) {
         return NULL;
     }
     PyDictObject *mp = (PyDictObject *)op;
@@ -2392,7 +2401,7 @@ _PyDict_GetItem_KnownHash(PyObject *op, PyObject *key, 
Py_hash_t hash)
     PyDictObject *mp = (PyDictObject *)op;
     PyObject *value;
 
-    if (!PyDict_Check(op)) {
+    if (!PyAnyDict_Check(op)) {
         PyErr_BadInternalCall();
         return NULL;
     }
@@ -2463,7 +2472,7 @@ _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject 
*key, Py_hash_t hash, Py
 int
 PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result)
 {
-    if (!PyDict_Check(op)) {
+    if (!PyAnyDict_Check(op)) {
         PyErr_BadInternalCall();
         *result = NULL;
         return -1;
@@ -2519,7 +2528,7 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key)
     PyDictObject*mp = (PyDictObject *)op;
     PyObject *value;
 
-    if (!PyDict_Check(op)) {
+    if (!PyAnyDict_Check(op)) {
         PyErr_BadInternalCall();
         return NULL;
     }
@@ -2668,7 +2677,7 @@ setitem_take2_lock_held(PyDictObject *mp, PyObject *key, 
PyObject *value)
 
     assert(key);
     assert(value);
-    assert(PyDict_Check(mp));
+    assert(PyAnyDict_Check(mp));
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
         dict_unhashable_type(key);
@@ -2713,6 +2722,16 @@ PyDict_SetItem(PyObject *op, PyObject *key, PyObject 
*value)
                                  Py_NewRef(key), Py_NewRef(value));
 }
 
+static int
+_PyAnyDict_SetItem(PyObject *op, PyObject *key, PyObject *value)
+{
+    assert(PyAnyDict_Check(op));
+    assert(key);
+    assert(value);
+    return _PyDict_SetItem_Take2((PyDictObject *)op,
+                                 Py_NewRef(key), Py_NewRef(value));
+}
+
 static int
 setitem_lock_held(PyDictObject *mp, PyObject *key, PyObject *value)
 {
@@ -2996,7 +3015,7 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject 
**pkey,
     PyObject *key, *value;
     Py_hash_t hash;
 
-    if (!PyDict_Check(op))
+    if (!PyAnyDict_Check(op))
         return 0;
 
     mp = (PyDictObject *)op;
@@ -3265,8 +3284,8 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, 
PyObject *value)
         return NULL;
 
 
-    if (PyDict_CheckExact(d)) {
-        if (PyDict_CheckExact(iterable)) {
+    if (PyAnyDict_CheckExact(d)) {
+        if (PyAnyDict_CheckExact(iterable)) {
             PyDictObject *mp = (PyDictObject *)d;
 
             Py_BEGIN_CRITICAL_SECTION2(d, iterable);
@@ -3290,7 +3309,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, 
PyObject *value)
         return NULL;
     }
 
-    if (PyDict_CheckExact(d)) {
+    if (PyAnyDict_CheckExact(d)) {
         Py_BEGIN_CRITICAL_SECTION(d);
         while ((key = PyIter_Next(it)) != NULL) {
             status = setitem_lock_held((PyDictObject *)d, key, value);
@@ -3460,7 +3479,7 @@ dict_repr(PyObject *self)
 static Py_ssize_t
 dict_length(PyObject *self)
 {
-    return FT_ATOMIC_LOAD_SSIZE_RELAXED(((PyDictObject *)self)->ma_used);
+    return GET_USED(_PyAnyDict_CAST(self));
 }
 
 static PyObject *
@@ -3480,7 +3499,7 @@ dict_subscript(PyObject *self, PyObject *key)
     if (ix == DKIX_ERROR)
         return NULL;
     if (ix == DKIX_EMPTY || value == NULL) {
-        if (!PyDict_CheckExact(mp)) {
+        if (!PyAnyDict_CheckExact(mp)) {
             /* Look up __missing__ method if we're a subclass. */
             PyObject *missing, *res;
             missing = _PyObject_LookupSpecial(
@@ -3519,7 +3538,7 @@ keys_lock_held(PyObject *dict)
 {
     ASSERT_DICT_LOCKED(dict);
 
-    if (dict == NULL || !PyDict_Check(dict)) {
+    if (dict == NULL || !PyAnyDict_Check(dict)) {
         PyErr_BadInternalCall();
         return NULL;
     }
@@ -3568,7 +3587,7 @@ values_lock_held(PyObject *dict)
 {
     ASSERT_DICT_LOCKED(dict);
 
-    if (dict == NULL || !PyDict_Check(dict)) {
+    if (dict == NULL || !PyAnyDict_Check(dict)) {
         PyErr_BadInternalCall();
         return NULL;
     }
@@ -3616,7 +3635,7 @@ items_lock_held(PyObject *dict)
 {
     ASSERT_DICT_LOCKED(dict);
 
-    if (dict == NULL || !PyDict_Check(dict)) {
+    if (dict == NULL || !PyAnyDict_Check(dict)) {
         PyErr_BadInternalCall();
         return NULL;
     }
@@ -3696,7 +3715,7 @@ dict_fromkeys_impl(PyTypeObject *type, PyObject 
*iterable, PyObject *value)
 static int
 dict_update_arg(PyObject *self, PyObject *arg)
 {
-    if (PyDict_CheckExact(arg)) {
+    if (PyAnyDict_CheckExact(arg)) {
         return PyDict_Merge(self, arg, 1);
     }
     int has_keys = PyObject_HasAttrWithError(arg, &_Py_ID(keys));
@@ -3762,7 +3781,7 @@ merge_from_seq2_lock_held(PyObject *d, PyObject *seq2, 
int override)
     PyObject *fast;     /* item as a 2-tuple or 2-list */
 
     assert(d != NULL);
-    assert(PyDict_Check(d));
+    assert(PyAnyDict_Check(d));
     assert(seq2 != NULL);
 
     it = PyObject_GetIter(seq2);
@@ -3958,13 +3977,13 @@ dict_merge(PyObject *a, PyObject *b, int override)
      * things quite efficiently.  For the latter, we only require that
      * PyMapping_Keys() and PyObject_GetItem() be supported.
      */
-    if (a == NULL || !PyDict_Check(a) || b == NULL) {
+    if (a == NULL || !PyAnyDict_Check(a) || b == NULL) {
         PyErr_BadInternalCall();
         return -1;
     }
     mp = (PyDictObject*)a;
     int res = 0;
-    if (PyDict_Check(b) && (Py_TYPE(b)->tp_iter == dict_iter)) {
+    if (PyAnyDict_Check(b) && (Py_TYPE(b)->tp_iter == dict_iter)) {
         other = (PyDictObject*)b;
         int res;
         Py_BEGIN_CRITICAL_SECTION2(a, b);
@@ -4075,6 +4094,9 @@ static PyObject *
 dict_copy_impl(PyDictObject *self)
 /*[clinic end generated code: output=ffb782cf970a5c39 input=73935f042b639de4]*/
 {
+    if (PyFrozenDict_CheckExact(self)) {
+        return Py_NewRef(self);
+    }
     return PyDict_Copy((PyObject *)self);
 }
 
@@ -4104,13 +4126,19 @@ copy_lock_held(PyObject *o)
 {
     PyObject *copy;
     PyDictObject *mp;
+    int frozendict = PyFrozenDict_Check(o);
 
     ASSERT_DICT_LOCKED(o);
 
     mp = (PyDictObject *)o;
     if (mp->ma_used == 0) {
         /* The dict is empty; just return a new dict. */
-        return PyDict_New();
+        if (frozendict) {
+            return PyFrozenDict_New(NULL);
+        }
+        else {
+            return PyDict_New();
+        }
     }
 
     if (_PyDict_HasSplitTable(mp)) {
@@ -4119,7 +4147,13 @@ copy_lock_held(PyObject *o)
         if (newvalues == NULL) {
             return PyErr_NoMemory();
         }
-        split_copy = PyObject_GC_New(PyDictObject, &PyDict_Type);
+        if (frozendict) {
+            split_copy = (PyDictObject *)PyObject_GC_New(PyFrozenDictObject,
+                                                         &PyFrozenDict_Type);
+        }
+        else {
+            split_copy = PyObject_GC_New(PyDictObject, &PyDict_Type);
+        }
         if (split_copy == NULL) {
             free_values(newvalues, false);
             return NULL;
@@ -4132,13 +4166,18 @@ copy_lock_held(PyObject *o)
         split_copy->ma_used = mp->ma_used;
         split_copy->_ma_watcher_tag = 0;
         dictkeys_incref(mp->ma_keys);
+        if (frozendict) {
+            PyFrozenDictObject *frozen = (PyFrozenDictObject *)split_copy;
+            frozen->ma_hash = -1;
+        }
         _PyObject_GC_TRACK(split_copy);
         return (PyObject *)split_copy;
     }
 
     if (Py_TYPE(mp)->tp_iter == dict_iter &&
             mp->ma_values == NULL &&
-            (mp->ma_used >= (mp->ma_keys->dk_nentries * 2) / 3))
+            (mp->ma_used >= (mp->ma_keys->dk_nentries * 2) / 3) &&
+            !frozendict)
     {
         /* Use fast-copy if:
 
@@ -4170,7 +4209,12 @@ copy_lock_held(PyObject *o)
         return (PyObject *)new;
     }
 
-    copy = PyDict_New();
+    if (frozendict) {
+        copy = PyFrozenDict_New(NULL);
+    }
+    else {
+        copy = PyDict_New();
+    }
     if (copy == NULL)
         return NULL;
     if (dict_merge(copy, o, 1) == 0)
@@ -4182,7 +4226,7 @@ copy_lock_held(PyObject *o)
 PyObject *
 PyDict_Copy(PyObject *o)
 {
-    if (o == NULL || !PyDict_Check(o)) {
+    if (o == NULL || !PyAnyDict_Check(o)) {
         PyErr_BadInternalCall();
         return NULL;
     }
@@ -4199,11 +4243,11 @@ PyDict_Copy(PyObject *o)
 Py_ssize_t
 PyDict_Size(PyObject *mp)
 {
-    if (mp == NULL || !PyDict_Check(mp)) {
+    if (mp == NULL || !PyAnyDict_Check(mp)) {
         PyErr_BadInternalCall();
         return -1;
     }
-    return FT_ATOMIC_LOAD_SSIZE_RELAXED(((PyDictObject *)mp)->ma_used);
+    return GET_USED((PyDictObject *)mp);
 }
 
 /* Return 1 if dicts equal, 0 if not, -1 if error.
@@ -4289,7 +4333,7 @@ dict_richcompare(PyObject *v, PyObject *w, int op)
     int cmp;
     PyObject *res;
 
-    if (!PyDict_Check(v) || !PyDict_Check(w)) {
+    if (!PyAnyDict_Check(v) || !PyAnyDict_Check(w)) {
         res = Py_NotImplemented;
     }
     else if (op == Py_EQ || op == Py_NE) {
@@ -4739,7 +4783,7 @@ dict___sizeof___impl(PyDictObject *self)
 static PyObject *
 dict_or(PyObject *self, PyObject *other)
 {
-    if (!PyDict_Check(self) || !PyDict_Check(other)) {
+    if (!PyAnyDict_Check(self) || !PyAnyDict_Check(other)) {
         Py_RETURN_NOTIMPLEMENTED;
     }
     PyObject *new = PyDict_Copy(self);
@@ -4753,6 +4797,29 @@ dict_or(PyObject *self, PyObject *other)
     return new;
 }
 
+static PyObject *
+frozendict_or(PyObject *self, PyObject *other)
+{
+    if (PyFrozenDict_CheckExact(self)) {
+        // frozendict() | frozendict(...) => frozendict(...)
+        if (GET_USED((PyDictObject *)self) == 0
+            && PyFrozenDict_CheckExact(other))
+        {
+            return Py_NewRef(other);
+        }
+
+        // frozendict(...) | frozendict() => frozendict(...)
+        if (PyAnyDict_CheckExact(other)
+            && GET_USED((PyDictObject *)other) == 0)
+        {
+            return Py_NewRef(self);
+        }
+    }
+
+    return dict_or(self, other);
+}
+
+
 static PyObject *
 dict_ior(PyObject *self, PyObject *other)
 {
@@ -4905,7 +4972,15 @@ dict_vectorcall(PyObject *type, PyObject * const*args,
         return NULL;
     }
 
-    PyObject *self = dict_new(_PyType_CAST(type), NULL, NULL);
+    PyObject *self;
+    if (Py_Is((PyTypeObject*)type, &PyFrozenDict_Type)
+        || PyType_IsSubtype((PyTypeObject*)type, &PyFrozenDict_Type))
+    {
+        self = frozendict_new(_PyType_CAST(type), NULL, NULL);
+    }
+    else {
+        self = dict_new(_PyType_CAST(type), NULL, NULL);
+    }
     if (self == NULL) {
         return NULL;
     }
@@ -4918,7 +4993,8 @@ dict_vectorcall(PyObject *type, PyObject * const*args,
     }
     if (kwnames != NULL) {
         for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(kwnames); i++) {
-            if (PyDict_SetItem(self, PyTuple_GET_ITEM(kwnames, i), args[i]) < 
0) {
+            PyObject *key = PyTuple_GET_ITEM(kwnames, i);  // borrowed
+            if (_PyAnyDict_SetItem(self, key, args[i]) < 0) {
                 Py_DECREF(self);
                 return NULL;
             }
@@ -4991,6 +5067,7 @@ PyTypeObject PyDict_Type = {
     .tp_version_tag = _Py_TYPE_VERSION_DICT,
 };
 
+
 /* For backward compatibility with old dictionary interface */
 
 PyObject *
@@ -5073,7 +5150,7 @@ dictiter_new(PyDictObject *dict, PyTypeObject *itertype)
         return NULL;
     }
     di->di_dict = (PyDictObject*)Py_NewRef(dict);
-    used = FT_ATOMIC_LOAD_SSIZE_RELAXED(dict->ma_used);
+    used = GET_USED(dict);
     di->di_used = used;
     di->len = used;
     if (itertype == &PyDictRevIterKey_Type ||
@@ -5129,7 +5206,7 @@ dictiter_len(PyObject *self, PyObject *Py_UNUSED(ignored))
 {
     dictiterobject *di = (dictiterobject *)self;
     Py_ssize_t len = 0;
-    if (di->di_dict != NULL && di->di_used == 
FT_ATOMIC_LOAD_SSIZE_RELAXED(di->di_dict->ma_used))
+    if (di->di_dict != NULL && di->di_used == GET_USED(di->di_dict))
         len = FT_ATOMIC_LOAD_SSIZE_RELAXED(di->len);
     return PyLong_FromSize_t(len);
 }
@@ -5166,7 +5243,7 @@ dictiter_iternextkey_lock_held(PyDictObject *d, PyObject 
*self)
     Py_ssize_t i;
     PyDictKeysObject *k;
 
-    assert (PyDict_Check(d));
+    assert (PyAnyDict_Check(d));
     ASSERT_DICT_LOCKED(d);
 
     if (di->di_used != d->ma_used) {
@@ -5290,7 +5367,7 @@ dictiter_iternextvalue_lock_held(PyDictObject *d, 
PyObject *self)
     PyObject *value;
     Py_ssize_t i;
 
-    assert (PyDict_Check(d));
+    assert (PyAnyDict_Check(d));
     ASSERT_DICT_LOCKED(d);
 
     if (di->di_used != d->ma_used) {
@@ -5412,7 +5489,7 @@ dictiter_iternextitem_lock_held(PyDictObject *d, PyObject 
*self,
     PyObject *key, *value;
     Py_ssize_t i;
 
-    assert (PyDict_Check(d));
+    assert (PyAnyDict_Check(d));
     ASSERT_DICT_LOCKED(d);
 
     if (di->di_used != d->ma_used) {
@@ -5518,7 +5595,7 @@ dictiter_iternext_threadsafe(PyDictObject *d, PyObject 
*self,
     Py_ssize_t i;
     PyDictKeysObject *k;
 
-    assert (PyDict_Check(d));
+    assert (PyAnyDict_Check(d));
 
     if (di->di_used != _Py_atomic_load_ssize_relaxed(&d->ma_used)) {
         PyErr_SetString(PyExc_RuntimeError,
@@ -5712,7 +5789,7 @@ dictreviter_iter_lock_held(PyDictObject *d, PyObject 
*self)
 {
     dictiterobject *di = (dictiterobject *)self;
 
-    assert (PyDict_Check(d));
+    assert (PyAnyDict_Check(d));
     ASSERT_DICT_LOCKED(d);
 
     if (di->di_used != d->ma_used) {
@@ -5842,7 +5919,7 @@ static PyObject *
 dict___reversed___impl(PyDictObject *self)
 /*[clinic end generated code: output=e674483336d1ed51 input=23210ef3477d8c4d]*/
 {
-    assert (PyDict_Check(self));
+    assert (PyAnyDict_Check(self));
     return dictiter_new(self, &PyDictRevIterKey_Type);
 }
 
@@ -5915,7 +5992,7 @@ dictview_len(PyObject *self)
     _PyDictViewObject *dv = (_PyDictViewObject *)self;
     Py_ssize_t len = 0;
     if (dv->dv_dict != NULL)
-        len = FT_ATOMIC_LOAD_SSIZE_RELAXED(dv->dv_dict->ma_used);
+        len = GET_USED(dv->dv_dict);
     return len;
 }
 
@@ -5927,7 +6004,7 @@ _PyDictView_New(PyObject *dict, PyTypeObject *type)
         PyErr_BadInternalCall();
         return NULL;
     }
-    if (!PyDict_Check(dict)) {
+    if (!PyAnyDict_Check(dict)) {
         /* XXX Get rid of this restriction later */
         PyErr_Format(PyExc_TypeError,
                      "%s() requires a dict argument, not '%s'",
@@ -6117,7 +6194,7 @@ dictviews_to_set(PyObject *self)
     if (PyDictKeys_Check(self)) {
         // PySet_New() has fast path for the dict object.
         PyObject *dict = (PyObject *)((_PyDictViewObject *)self)->dv_dict;
-        if (PyDict_CheckExact(dict)) {
+        if (PyAnyDict_CheckExact(dict)) {
             left = dict;
         }
     }
@@ -6847,6 +6924,11 @@ _PyObject_MaterializeManagedDict(PyObject *obj)
 int
 _PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value)
 {
+    if (!PyDict_Check(dict)) {
+        PyErr_BadInternalCall();
+        return -1;
+    }
+
     if (value == NULL) {
         Py_hash_t hash = _PyObject_HashFast(name);
         if (hash == -1) {
@@ -7169,7 +7251,7 @@ _PyObject_IsInstanceDictEmpty(PyObject *obj)
     if (dict == NULL) {
         return 1;
     }
-    return FT_ATOMIC_LOAD_SSIZE_RELAXED(((PyDictObject *)dict)->ma_used) == 0;
+    return GET_USED((PyDictObject *)dict) == 0;
 }
 
 int
@@ -7631,7 +7713,7 @@ validate_watcher_id(PyInterpreterState *interp, int 
watcher_id)
 int
 PyDict_Watch(int watcher_id, PyObject* dict)
 {
-    if (!PyDict_Check(dict)) {
+    if (!PyAnyDict_Check(dict)) {
         PyErr_SetString(PyExc_ValueError, "Cannot watch non-dictionary");
         return -1;
     }
@@ -7646,7 +7728,7 @@ PyDict_Watch(int watcher_id, PyObject* dict)
 int
 PyDict_Unwatch(int watcher_id, PyObject* dict)
 {
-    if (!PyDict_Check(dict)) {
+    if (!PyAnyDict_Check(dict)) {
         PyErr_SetString(PyExc_ValueError, "Cannot watch non-dictionary");
         return -1;
     }
@@ -7743,3 +7825,147 @@ _PyObject_InlineValuesConsistencyCheck(PyObject *obj)
     return 0;
 }
 #endif
+
+// --- frozendict implementation ---------------------------------------------
+
+static PyNumberMethods frozendict_as_number = {
+    .nb_or = frozendict_or,
+};
+
+static PyMappingMethods frozendict_as_mapping = {
+    .mp_length = dict_length,
+    .mp_subscript = dict_subscript,
+};
+
+static PyMethodDef frozendict_methods[] = {
+    DICT___CONTAINS___METHODDEF
+    {"__getitem__", dict_subscript, METH_O | METH_COEXIST, getitem__doc__},
+    DICT___SIZEOF___METHODDEF
+    DICT_GET_METHODDEF
+    DICT_KEYS_METHODDEF
+    DICT_ITEMS_METHODDEF
+    DICT_VALUES_METHODDEF
+    DICT_FROMKEYS_METHODDEF
+    DICT_COPY_METHODDEF
+    DICT___REVERSED___METHODDEF
+    {"__class_getitem__", Py_GenericAlias, METH_O|METH_CLASS, PyDoc_STR("See 
PEP 585")},
+    {NULL,              NULL}   /* sentinel */
+};
+
+
+static PyObject *
+frozendict_repr(PyObject *self)
+{
+    PyObject *repr = dict_repr(self);
+    if (repr == NULL) {
+        return NULL;
+    }
+    assert(PyUnicode_Check(repr));
+
+    PyObject *res = PyUnicode_FromFormat("%s(%U)",
+                                         Py_TYPE(self)->tp_name,
+                                         repr);
+    Py_DECREF(repr);
+    return res;
+}
+
+static Py_hash_t
+frozendict_hash(PyObject *op)
+{
+    PyFrozenDictObject *self = _PyFrozenDictObject_CAST(op);
+    Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ma_hash);
+    if (hash != -1) {
+        return hash;
+    }
+
+    PyObject *items = _PyDictView_New(op, &PyDictItems_Type);
+    if (items == NULL) {
+        return -1;
+    }
+    PyObject *frozenset = PyFrozenSet_New(items);
+    Py_DECREF(items);
+    if (frozenset == NULL) {
+        return -1;
+    }
+
+    hash = PyObject_Hash(frozenset);
+    Py_DECREF(frozenset);
+    if (hash == -1) {
+        return -1;
+    }
+
+    FT_ATOMIC_STORE_SSIZE_RELAXED(self->ma_hash, hash);
+    return hash;
+}
+
+
+static PyObject *
+frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+    PyObject *d = dict_new(type, args, kwds);
+    if (d == NULL) {
+        return NULL;
+    }
+    PyFrozenDictObject *self = _PyFrozenDictObject_CAST(d);
+    self->ma_hash = -1;
+
+    if (args != NULL) {
+        if (dict_update_common(d, args, kwds, "frozendict") < 0) {
+            Py_DECREF(d);
+            return NULL;
+        }
+    }
+    else {
+        assert(kwds == NULL);
+    }
+
+    return d;
+}
+
+
+PyObject*
+PyFrozenDict_New(PyObject *iterable)
+{
+    if (iterable != NULL) {
+        PyObject *args = PyTuple_Pack(1, iterable);
+        if (args == NULL) {
+            return NULL;
+        }
+        PyObject *frozendict = frozendict_new(&PyFrozenDict_Type, args, NULL);
+        Py_DECREF(args);
+        return frozendict;
+    }
+    else {
+        PyObject *args = Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_TUPLE);
+        return frozendict_new(&PyFrozenDict_Type, args, NULL);
+    }
+}
+
+
+PyTypeObject PyFrozenDict_Type = {
+    PyVarObject_HEAD_INIT(&PyType_Type, 0)
+    .tp_name = "frozendict",
+    .tp_basicsize = sizeof(PyFrozenDictObject),
+    .tp_dealloc = dict_dealloc,
+    .tp_repr = frozendict_repr,
+    .tp_as_number = &frozendict_as_number,
+    .tp_as_sequence = &dict_as_sequence,
+    .tp_as_mapping = &frozendict_as_mapping,
+    .tp_hash = frozendict_hash,
+    .tp_getattro = PyObject_GenericGetAttr,
+    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC
+                | Py_TPFLAGS_BASETYPE
+                | _Py_TPFLAGS_MATCH_SELF | Py_TPFLAGS_MAPPING,
+    .tp_doc = dictionary_doc,
+    .tp_traverse = dict_traverse,
+    .tp_clear = dict_tp_clear,
+    .tp_richcompare = dict_richcompare,
+    .tp_iter = dict_iter,
+    .tp_methods = frozendict_methods,
+    .tp_init = dict_init,
+    .tp_alloc = _PyType_AllocNoTrack,
+    .tp_new = frozendict_new,
+    .tp_free = PyObject_GC_Del,
+    .tp_vectorcall = dict_vectorcall,
+    .tp_version_tag = _Py_TYPE_VERSION_FROZENDICT,
+};
diff --git a/Objects/object.c b/Objects/object.c
index 1ddd949d28143e..ab73d2eb1c9c1f 100644
--- a/Objects/object.c
+++ b/Objects/object.c
@@ -58,7 +58,7 @@ _PyObject_CheckConsistency(PyObject *op, int check_content)
     if (PyUnicode_Check(op)) {
         _PyUnicode_CheckConsistency(op, check_content);
     }
-    else if (PyDict_Check(op)) {
+    else if (PyAnyDict_Check(op)) {
         _PyDict_CheckConsistency(op, check_content);
     }
     return 1;
@@ -2532,8 +2532,9 @@ static PyTypeObject* static_types[] = {
     &PyEnum_Type,
     &PyFilter_Type,
     &PyFloat_Type,
-    &PyFrame_Type,
     &PyFrameLocalsProxy_Type,
+    &PyFrame_Type,
+    &PyFrozenDict_Type,
     &PyFrozenSet_Type,
     &PyFunction_Type,
     &PyGen_Type,
diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c
index 9144793ae73ce1..493a6e0413d8eb 100644
--- a/Python/bltinmodule.c
+++ b/Python/bltinmodule.c
@@ -3536,6 +3536,7 @@ _PyBuiltin_Init(PyInterpreterState *interp)
     SETBUILTIN("enumerate",             &PyEnum_Type);
     SETBUILTIN("filter",                &PyFilter_Type);
     SETBUILTIN("float",                 &PyFloat_Type);
+    SETBUILTIN("frozendict",            &PyFrozenDict_Type);
     SETBUILTIN("frozenset",             &PyFrozenSet_Type);
     SETBUILTIN("property",              &PyProperty_Type);
     SETBUILTIN("int",                   &PyLong_Type);
diff --git a/Python/marshal.c b/Python/marshal.c
index 190fcdc89afaa8..a71909f103ebfc 100644
--- a/Python/marshal.c
+++ b/Python/marshal.c
@@ -67,6 +67,7 @@ module marshal
 #define TYPE_TUPLE              '('  // See also TYPE_SMALL_TUPLE.
 #define TYPE_LIST               '['
 #define TYPE_DICT               '{'
+#define TYPE_FROZENDICT         '}'
 #define TYPE_CODE               'c'
 #define TYPE_UNICODE            'u'
 #define TYPE_UNKNOWN            '?'
@@ -575,10 +576,15 @@ w_complex_object(PyObject *v, char flag, WFILE *p)
             w_object(PyList_GET_ITEM(v, i), p);
         }
     }
-    else if (PyDict_CheckExact(v)) {
+    else if (PyAnyDict_CheckExact(v)) {
         Py_ssize_t pos;
         PyObject *key, *value;
-        W_TYPE(TYPE_DICT, p);
+        if (PyFrozenDict_CheckExact(v)) {
+            W_TYPE(TYPE_FROZENDICT, p);
+        }
+        else {
+            W_TYPE(TYPE_DICT, p);
+        }
         /* This one is NULL object terminated! */
         pos = 0;
         while (PyDict_Next(v, &pos, &key, &value)) {
@@ -1420,6 +1426,7 @@ r_object(RFILE *p)
         break;
 
     case TYPE_DICT:
+    case TYPE_FROZENDICT:
         v = PyDict_New();
         R_REF(v);
         if (v == NULL)
@@ -1443,7 +1450,16 @@ r_object(RFILE *p)
             Py_DECREF(val);
         }
         if (PyErr_Occurred()) {
-            Py_SETREF(v, NULL);
+            Py_CLEAR(v);
+        }
+        if (type == TYPE_FROZENDICT && v != NULL) {
+            PyObject *frozendict = PyFrozenDict_New(v);
+            if (frozendict != NULL) {
+                Py_SETREF(v, frozendict);
+            }
+            else {
+                Py_CLEAR(v);
+            }
         }
         retval = v;
         break;
diff --git a/Tools/c-analyzer/cpython/ignored.tsv 
b/Tools/c-analyzer/cpython/ignored.tsv
index 91bbf94990ecc1..cbec0bf262f0e0 100644
--- a/Tools/c-analyzer/cpython/ignored.tsv
+++ b/Tools/c-analyzer/cpython/ignored.tsv
@@ -769,6 +769,7 @@ Modules/clinic/md5module.c.h        _md5_md5        
_keywords       -
 Modules/clinic/grpmodule.c.h   grp_getgrgid    _keywords       -
 Modules/clinic/grpmodule.c.h   grp_getgrnam    _keywords       -
 Objects/object.c       -       constants       static PyObject*[]
+Objects/dictobject.c   -       PyFrozenDict_Type       -
 
 
 ## False positives

_______________________________________________
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