diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad28f35ce2a..a003f6914b4 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 # ... diff --git a/peps/pep-0814.rst b/peps/pep-0814.rst new file mode 100644 index 00000000000..d35cc8bbd80 --- /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: 12-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'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 third-party ``frozendict`` and ``frozenmap`` packages +available on PyPI, proving that there is demand 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 either. 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 ``frozendict``\ s:: + + 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 similar "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 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. + + +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 has 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.