From 8e55c2863541d73537505bf2c5e3694f324fb1cc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 12 Nov 2025 10:40:40 +0100 Subject: [PATCH 1/7] PEP 814: Add frozendict built-in type --- peps/pep-0814.rst | 347 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 peps/pep-0814.rst diff --git a/peps/pep-0814.rst b/peps/pep-0814.rst new file mode 100644 index 00000000000..118f399d8df --- /dev/null +++ b/peps/pep-0814.rst @@ -0,0 +1,347 @@ +PEP: 814 +Title: Add frozendict built-in type +Author: Victor Stinner , Donghee Na +Status: Draft +Type: Standards Track +Created: 06-Nov-2025 +Python-Version: 3.15 + +Abstract +======== + +A new public immutable type ``frozendict`` is added to the ``builtins`` +module. + +We expect frozendict to be safe by design, as it prevents any unintended +modifications. This addition benefits not only CPython’s standard +library, but also third-party maintainers who can take advantage of a +reliable, immutable dictionary type. + + +Rationale +========= + +The proposed ``frozendict`` type: + +* implements the ``collections.abc.Mapping`` protocol, +* supports pickling. + +The following use cases illustrate why an immutable mapping is +desirable: + +* Immutable mappings are hashable which allows their use as dictionary + keys or set elements. + +* This hashable property permits functions decorated with + ``@functools.lru_cache()`` to accept immutable mappings as arguments. + Unlike an immutable mapping, passing a plain dict to such a function + results in error. + +* Using an immutable mapping as a function parameter default value + avoids the problem of mutable default values. + +* Immutable mappings can be used to safely share dictionaries across + thread and asynchronous task boundaries. The immutability makes it + easier to reason about threads and asynchronous tasks. + +There are already multiple existing 3rd party ``frozendict`` and +``frozenmap`` available on PyPI, proving that there is a need for +immutable mappings. + + +Specification +============= + +A new public immutable type ``frozendict`` is added to the ``builtins`` +module. It is not a ``dict`` subclass but inherits directly from +``object``. + + +Construction +------------ + +``frozendict`` implements a dict-like construction API: + +* ``frozendict()`` creates a new empty immutable mapping; + +* ``frozendict(**kwargs)`` creates a mapping from ``**kwargs``, + e.g. ``frozendict(x=1, y=2)``. + +* ``frozendict(collection)`` creates a mapping from the passed + collection object. The passed collection object can be: + + - a ``dict``, + - another ``frozendict``, + - or an iterable of key/value tuples. + +The insertion order is preserved. + + +Iteration +--------- + +As ``frozendict`` implements the standard ``collections.abc.Mapping`` +protocol, so all expected methods of iteration are supported:: + + assert list(m.items()) == [('foo', 'bar')] + assert list(m.keys()) == ['foo'] + assert list(m.values()) == ['bar'] + assert list(m) == ['foo'] + +Iterating on ``frozendict``, as on ``dict``, uses the insertion order. + + +Hashing +------- + +``frozendict`` instances can be hashable just like tuple objects:: + + hash(frozendict(foo='bar')) # works + hash(frozendict(foo=['a', 'b', 'c'])) # error, list is not hashable + +The hash value does not depend on the items order. It is computed on +keys and values. Pseudo-code of ``hash(frozendict)``:: + + hash(frozenset(frozendict.items())) + +Equality test does not depend on the items order neither. Example:: + + >>> a = frozendict(x=1, y=2) + >>> b = frozendict(y=2, x=1) + >>> hash(a) == hash(b) + True + >>> a == b + True + + +Typing +------ + +It is possible to use the standard typing notation for frozendicts:: + + m: frozendict[str, int] = frozendict(x=1) + + +Representation +-------------- + +``frozendict`` will not use a special syntax for its representation. +The ``repr()`` of a ``frozendict`` instance looks like this: + + >>> frozendict(x=1, y=2) + frozendict({'x': 1, 'y': 2}) + + +C API +----- + +Add the following APIs: + +* ``PyFrozenDict_Type`` +* ``PyFrozenDict_New(collection)`` function +* ``PyFrozenDict_Check()`` macro +* ``PyFrozenDict_CheckExact()`` macro + +Even if ``frozendict`` is not a ``dict`` subclass, it can be used with +``PyDict_GetItemRef()`` and similiar "PyDict_Get" functions. + +Passing a ``frozendict`` to ``PyDict_SetItem()`` or ``PyDict_DelItem()`` +fails with ``TypeError``. ``PyDict_Check()`` on a ``frozendict`` is +false. + +Exposing the C API will help authors of C extensions to support +``frozendict`` in their extensions when they need to support immutable +containers to make thread-safe very easily. It will be important since +:pep:`779` (Criteria for supported status for free-threaded Python) was +accepted, people need this for their migration. + + +Differences between dict and frozendict +======================================= + +* ``dict`` has more methods than ``frozendict``: + + * ``__delitem__(key)`` + * ``__setitem__(key, value)`` + * ``clear()`` + * ``pop(key)`` + * ``popitem()`` + * ``setdefault(key, value)`` + * ``update(*args, **kwargs)`` + +* A ``frozendict`` can be hashed with ``hash(frozendict)`` if all keys + and values can be hashed. + + +Possible candidates for frozendict in the stdlib +================================================ + +We have identified several stdlib modules where adopting ``frozendict`` +can enhance safety and prevent unintended modifications by design. We +also believe that there are additional potential use cases beyond the +ones listed below. + +Note: it remains possible to bind again a variable to a new modified +``frozendict`` or a new mutable ``dict``. + +Python modules +-------------- + +Replace ``dict`` with ``frozendict`` in function results: + +* ``email.headerregistry``: ``ParameterizedMIMEHeader.params()`` + (replace ``MappingProxyType``) +* ``enum``: ``EnumType.__members__()`` (replace ``MappingProxyType``) + +Replace ``dict`` with ``frozendict`` for constants: + +* ``_opcode_metadata``: ``_specializations``, ``_specialized_opmap``, + ``opmap`` +* ``_pydatetime``: ``specs`` (in ``_format_time()``) +* ``_pydecimal``: ``_condition_map`` +* ``bdb``: ``_MonitoringTracer.EVENT_CALLBACK_MAP`` +* ``dataclasses``: ``_hash_action`` +* ``dis``: ``deoptmap``, ``COMPILER_FLAG_NAMES`` +* ``functools``: ``_convert`` +* ``gettext``: ``_binary_ops``, ``_c2py_ops`` +* ``imaplib``: ``Commands``, ``Mon2num`` +* ``json.decoder``: ``_CONSTANTS``, ``BACKSLASH`` +* ``json.encoder``: ``ESCAPE_DCT`` +* ``json.tool``: ``_group_to_theme_color`` +* ``locale``: ``locale_encoding_alias``, ``locale_alias``, + ``windows_locale`` +* ``opcode``: ``_cache_format``, ``_inline_cache_entries`` +* ``optparse``: ``_builtin_cvt`` +* ``platform``: ``_ver_stages``, ``_default_architecture`` +* ``plistlib``: ``_BINARY_FORMAT`` +* ``ssl``: ``_PROTOCOL_NAMES`` +* ``stringprep``: ``b3_exceptions`` +* ``symtable``: ``_scopes_value_to_name`` +* ``tarfile``: ``PAX_NUMBER_FIELDS``, ``_NAMED_FILTERS`` +* ``token``: ``tok_name``, ``EXACT_TOKEN_TYPES`` +* ``tomllib._parser``: ``BASIC_STR_ESCAPE_REPLACEMENTS`` +* ``typing``: ``_PROTO_ALLOWLIST`` + +Extension modules +----------------- + +Replace ``dict`` with ``frozendict`` for constants: + +* ``errno``: ``errorcode`` + + +Relationship to PEP 416 frozendict +================================== + +Since 2012 (PEP 416), the Python ecosystem evolved: + +* ``asyncio`` was added in 2014 (Python 3.4) +* Free Threading was added in 2024 (Python 3.13) +* ``concurrent.interpreters`` was added in 2025 (Python 3.14) + +There are now more use cases to share immutable mappings. + +``frozendict`` now preserves the insertion order, whereas PEP 416 +``frozendict`` was unordered (as PEP 603 ``frozenmap``). ``frozendict`` +relies on the ``dict`` implementation which preserves the insertion +order since Python 3.6. + +The first motivation to add ``frozendict`` was to implement a sandbox +in Python. It's no longer the case in this PEP. + +``types.MappingProxyType`` was added in 2012 (Python 3.3). This type is +not hashable and it's not possible to inherit from it. It's also easy to +retrieve the original dictionary which can be mutated, for example using +``gc.get_referents()``. + + +Relationship to PEP 603 frozenmap +================================= + +``collections.frozenmap`` has different properties than frozendict: + +* ``frozenmap`` items are unordered, whereas ``frozendict`` preserves + the insertion order. +* ``frozenmap`` has additional methods: + + * ``including(key, value)`` + * ``excluding(key)`` + * ``union(mapping=None, **kw)`` + +========== ============= ============== +Complexity ``frozenmap`` ``frozendict`` +========== ============= ============== +Lookup O(log n) O(1) +Copy O(1) O(n) +========== ============= ============== + + +Reference Implementation +======================== + +* The reference implementation is still a work-in-progress. +* ``frozendict`` shares most of its code with the ``dict`` type. +* Add ``PyFrozenDictObject`` which inherits from ``PyDictObject`` and + has an additional ``ma_hash`` member. + + +Thread Safety +============= + +Once the ``frozendict`` is created, it is immutable and can be shared +safely between threads without any synchronization. + + +Future Work +=========== + +We are also going to make ``frozendict`` to be more efficient in terms +of memory usage and performance compared to ``dict`` in future. + + +Rejected Ideas +============== + +Inherit from dict +----------------- + +If ``frozendict`` inherits from ``dict``, it would become possible to +call ``dict`` methods to mutate an immutable ``frozendict``. For +example, it would be possible to call +``dict.__setitem__(frozendict, key, value)``. + +It may be possible to prevent modifying ``frozendict`` using ``dict`` +methods, but that would require to explicitly exclude ``frozendict`` +which can affect ``dict`` performance. Also, there is a higher risk of +forgetting to exclude ``frozendict`` in some methods. + +If ``frozendict`` does not inherit from ``dict``, there is no such +issue. + + +New syntax for frozendict literals +---------------------------------- + +Various syntaxes have been proposed to write ``frozendict`` literals. + +A new syntax can be added later if needed. + + +References +========== + +* :pep:`416` (``frozendict``) +* :pep:`603` (``collections.frozenmap``) + + +Acknowledgements +================ + +This PEP is based on prior work from Yury Selivanov (PEP 603). + + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. From e8afe83a0fb0f301649224049ad6f080d1fbe02e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 12 Nov 2025 11:00:34 +0100 Subject: [PATCH 2/7] Update CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad28f35ce2a..3ead7ae78c8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -687,6 +687,7 @@ peps/pep-0807.rst @dstufft peps/pep-0809.rst @zooba peps/pep-0810.rst @pablogsal @DinoV @Yhg1s peps/pep-0811.rst @sethmlarson @gpshead +peps/pep-0814.rst @vstinner, @corona10 # ... peps/pep-2026.rst @hugovk # ... From aea7b3610bba5bfce83f73ddd5b904ab2de95641 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 12 Nov 2025 12:04:52 +0100 Subject: [PATCH 3/7] Update .github/CODEOWNERS Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3ead7ae78c8..a003f6914b4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -687,7 +687,7 @@ peps/pep-0807.rst @dstufft peps/pep-0809.rst @zooba peps/pep-0810.rst @pablogsal @DinoV @Yhg1s peps/pep-0811.rst @sethmlarson @gpshead -peps/pep-0814.rst @vstinner, @corona10 +peps/pep-0814.rst @vstinner @corona10 # ... peps/pep-2026.rst @hugovk # ... From fc4e926736dcb60ee6a89bb85646a4dff2df15f2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 12 Nov 2025 12:05:05 +0100 Subject: [PATCH 4/7] Update peps/pep-0814.rst Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0814.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0814.rst b/peps/pep-0814.rst index 118f399d8df..4392c2e86e7 100644 --- a/peps/pep-0814.rst +++ b/peps/pep-0814.rst @@ -3,7 +3,7 @@ Title: Add frozendict built-in type Author: Victor Stinner , Donghee Na Status: Draft Type: Standards Track -Created: 06-Nov-2025 +Created: 12-Nov-2025 Python-Version: 3.15 Abstract From ad1ef65f5206966278f0b0f87712dd1abd2bb0d7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 12 Nov 2025 12:10:14 +0100 Subject: [PATCH 5/7] Apply suggestions from code review Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- peps/pep-0814.rst | 54 +++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/peps/pep-0814.rst b/peps/pep-0814.rst index 4392c2e86e7..67c5f1533c3 100644 --- a/peps/pep-0814.rst +++ b/peps/pep-0814.rst @@ -12,7 +12,7 @@ Abstract A new public immutable type ``frozendict`` is added to the ``builtins`` module. -We expect frozendict to be safe by design, as it prevents any unintended +We expect ``frozendict`` to be safe by design, as it prevents any unintended modifications. This addition benefits not only CPython’s standard library, but also third-party maintainers who can take advantage of a reliable, immutable dictionary type. @@ -34,18 +34,18 @@ desirable: * This hashable property permits functions decorated with ``@functools.lru_cache()`` to accept immutable mappings as arguments. - Unlike an immutable mapping, passing a plain dict to such a function + Unlike an immutable mapping, passing a plain ``dict`` to such a function results in error. -* Using an immutable mapping as a function parameter default value +* Using an immutable mapping as a function parameter's default value avoids the problem of mutable default values. * Immutable mappings can be used to safely share dictionaries across thread and asynchronous task boundaries. The immutability makes it easier to reason about threads and asynchronous tasks. -There are already multiple existing 3rd party ``frozendict`` and -``frozenmap`` available on PyPI, proving that there is a need for +There are already third-party ``frozendict`` and ``frozenmap`` packages +available on PyPI, proving that there is demand for immutable mappings. @@ -60,9 +60,9 @@ module. It is not a ``dict`` subclass but inherits directly from Construction ------------ -``frozendict`` implements a dict-like construction API: +``frozendict`` implements a ``dict``-like construction API: -* ``frozendict()`` creates a new empty immutable mapping; +* ``frozendict()`` creates a new empty immutable mapping. * ``frozendict(**kwargs)`` creates a mapping from ``**kwargs``, e.g. ``frozendict(x=1, y=2)``. @@ -99,12 +99,12 @@ Hashing hash(frozendict(foo='bar')) # works hash(frozendict(foo=['a', 'b', 'c'])) # error, list is not hashable -The hash value does not depend on the items order. It is computed on +The hash value does not depend on the items' order. It is computed on keys and values. Pseudo-code of ``hash(frozendict)``:: hash(frozenset(frozendict.items())) -Equality test does not depend on the items order neither. Example:: +Equality test does not depend on the items' order neither. Example:: >>> a = frozendict(x=1, y=2) >>> b = frozendict(y=2, x=1) @@ -117,7 +117,7 @@ Equality test does not depend on the items order neither. Example:: Typing ------ -It is possible to use the standard typing notation for frozendicts:: +It is possible to use the standard typing notation for ``frozendict``\ s:: m: frozendict[str, int] = frozendict(x=1) @@ -143,7 +143,7 @@ Add the following APIs: * ``PyFrozenDict_CheckExact()`` macro Even if ``frozendict`` is not a ``dict`` subclass, it can be used with -``PyDict_GetItemRef()`` and similiar "PyDict_Get" functions. +``PyDict_GetItemRef()`` and similar "PyDict_Get" functions. Passing a ``frozendict`` to ``PyDict_SetItem()`` or ``PyDict_DelItem()`` fails with ``TypeError``. ``PyDict_Check()`` on a ``frozendict`` is @@ -156,8 +156,8 @@ containers to make thread-safe very easily. It will be important since accepted, people need this for their migration. -Differences between dict and frozendict -======================================= +Differences between ``dict`` and ``frozendict`` +=============================================== * ``dict`` has more methods than ``frozendict``: @@ -173,8 +173,8 @@ Differences between dict and frozendict and values can be hashed. -Possible candidates for frozendict in the stdlib -================================================ +Possible candidates for ``frozendict`` in the stdlib +==================================================== We have identified several stdlib modules where adopting ``frozendict`` can enhance safety and prevent unintended modifications by design. We @@ -233,16 +233,16 @@ Replace ``dict`` with ``frozendict`` for constants: Relationship to PEP 416 frozendict ================================== -Since 2012 (PEP 416), the Python ecosystem evolved: +Since 2012 (:pep:`416`), the Python ecosystem evolved: * ``asyncio`` was added in 2014 (Python 3.4) -* Free Threading was added in 2024 (Python 3.13) +* Free threading was added in 2024 (Python 3.13) * ``concurrent.interpreters`` was added in 2025 (Python 3.14) There are now more use cases to share immutable mappings. ``frozendict`` now preserves the insertion order, whereas PEP 416 -``frozendict`` was unordered (as PEP 603 ``frozenmap``). ``frozendict`` +``frozendict`` was unordered (as :pep:`603` ``frozenmap``). ``frozendict`` relies on the ``dict`` implementation which preserves the insertion order since Python 3.6. @@ -268,12 +268,12 @@ Relationship to PEP 603 frozenmap * ``excluding(key)`` * ``union(mapping=None, **kw)`` -========== ============= ============== -Complexity ``frozenmap`` ``frozendict`` -========== ============= ============== -Lookup O(log n) O(1) -Copy O(1) O(n) -========== ============= ============== +========== ============== ============== +Complexity ``frozenmap`` ``frozendict`` +========== ============== ============== +Lookup *O*\ (log *n*) *O*\ (1) +Copy *O*\ (1) *O*\ (*n*) +========== ============== ============== Reference Implementation @@ -319,8 +319,8 @@ If ``frozendict`` does not inherit from ``dict``, there is no such issue. -New syntax for frozendict literals ----------------------------------- +New syntax for ``frozendict`` literals +-------------------------------------- Various syntaxes have been proposed to write ``frozendict`` literals. @@ -337,7 +337,7 @@ References Acknowledgements ================ -This PEP is based on prior work from Yury Selivanov (PEP 603). +This PEP is based on prior work from Yury Selivanov (:pep:`603`). Copyright From de14e1af6e1b92cf595b6a09cbdd8047c4efea47 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 12 Nov 2025 13:50:43 +0100 Subject: [PATCH 6/7] PEP 814: Rephrase a sentence --- peps/pep-0814.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/peps/pep-0814.rst b/peps/pep-0814.rst index 67c5f1533c3..cdf3ebe949d 100644 --- a/peps/pep-0814.rst +++ b/peps/pep-0814.rst @@ -149,9 +149,9 @@ Passing a ``frozendict`` to ``PyDict_SetItem()`` or ``PyDict_DelItem()`` fails with ``TypeError``. ``PyDict_Check()`` on a ``frozendict`` is false. -Exposing the C API will help authors of C extensions to support -``frozendict`` in their extensions when they need to support immutable -containers to make thread-safe very easily. It will be important since +Exposing the C API helps authors of C extensions supporting +``frozendict`` when they need to support thread-safe immutable +containers. It will be important since :pep:`779` (Criteria for supported status for free-threaded Python) was accepted, people need this for their migration. From 1685d2f61a73146d1e45e280588712a8f4e1c91f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 12 Nov 2025 15:21:35 +0100 Subject: [PATCH 7/7] Address Nathan's review --- peps/pep-0814.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/peps/pep-0814.rst b/peps/pep-0814.rst index cdf3ebe949d..d35cc8bbd80 100644 --- a/peps/pep-0814.rst +++ b/peps/pep-0814.rst @@ -104,7 +104,7 @@ keys and values. Pseudo-code of ``hash(frozendict)``:: hash(frozenset(frozendict.items())) -Equality test does not depend on the items' order neither. Example:: +Equality test does not depend on the items' order either. Example:: >>> a = frozendict(x=1, y=2) >>> b = frozendict(y=2, x=1) @@ -233,7 +233,7 @@ Replace ``dict`` with ``frozendict`` for constants: Relationship to PEP 416 frozendict ================================== -Since 2012 (:pep:`416`), the Python ecosystem evolved: +Since 2012 (:pep:`416`), the Python ecosystem has evolved: * ``asyncio`` was added in 2014 (Python 3.4) * Free threading was added in 2024 (Python 3.13)