PEP: 814
Title: Add frozendict built-in type
Author: Victor Stinner <vstinner@python.org>, Donghee Na <donghee.na@python.org>
Discussions-To: https://discuss.python.org/t/104854
Status: Final
Type: Standards Track
Created: 12-Nov-2025
Python-Version: 3.15
Post-History: `13-Nov-2025 <https://discuss.python.org/t/104854>`__
Resolution: `11-Feb-2026 <https://discuss.python.org/t/pep-814-add-frozendict-built-in-type/104854/121>`__


.. canonical-doc:: :class:`frozendict`


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.

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.

* ``frozendict(collection, **kwargs)`` combines the two previous
  constructions.

Keys must be hashable, but values can be non-hashable.
Using hashable values creates a hashable ``frozendict``.

Creating a ``frozendict`` from a ``dict``, ``frozendict(dict)``, has a
complexity of *O*\ (*n*): items are copied (shallow copy).

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 and Comparison
----------------------

``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

It's possible to compare ``frozendict`` to ``dict``. Example::

    >>> frozendict(x=1, y=2) == dict(x=1, y=2)
    True


Union operators
---------------

It's possible to join two ``frozendict``, or a ``frozendict`` with a
``dict``, with the merge (``|``) operator. Example::

    >>> frozendict(x=1) | frozendict(y=1)
    frozendict({'x': 1, 'y': 1})
    >>> frozendict(x=1) | dict(y=1)
    frozendict({'x': 1, 'y': 1})

If some keys are in common, the values of the right operand are taken::

    >>> frozendict(x=1, y=2) | frozendict(y=5)
    frozendict({'x': 1, 'y': 5})

The update operator ``|=`` does not modify a ``frozendict`` in-place, but
creates a new ``frozendict``::

    >>> d = frozendict(x=1)
    >>> copy = d
    >>> d |= frozendict(y=2)
    >>> d
    frozendict({'x': 1, 'y': 2})
    >>> copy   # left unchanged
    frozendict({'x': 1})

See also :pep:`584` "Add Union Operators To dict".


Copy
----

``frozendict.copy()`` returns a shallow copy. In CPython, it simply
returns the same ``frozendict`` (new reference).

Use ``copy.deepcopy()`` to get a deep copy.

Example::

    >>> import copy
    >>> d = frozendict(mutable=[])
    >>> shallow_copy = d.copy()
    >>> deep_copy = copy.deepcopy(d)
    >>> d['mutable'].append('modified')
    >>> d
    frozendict({'mutable': ['modified']})
    >>> shallow_copy  # modified!
    frozendict({'mutable': ['modified']})
    >>> deep_copy     # unchanged
    frozendict({'mutable': []})


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:

* ``PyAnyDict_Check(op)`` macro
* ``PyAnyDict_CheckExact(op)`` macro
* ``PyFrozenDict_Check()`` macro
* ``PyFrozenDict_CheckExact()`` macro
* ``PyFrozenDict_New(collection)`` function
* ``PyFrozenDict_Type``

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.


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. However, this does not mean that we intend to make
these changes without the approval of the respective module maintainers.

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``

Accept ``frozendict`` type:

* ``builtins``: ``eval()`` and ``exec()`` (*globals* argument)

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)``

  These methods to mutate a ``frozenmap`` have a complexity of *O*\ (1).

* A mapping lookup (``mapping[key]``) has a complexity of *O*\ (log *n*)
  with ``frozenmap`` and a complexity of *O*\ (1) with ``frozendict``.


Reference Implementation
========================

* https://github.com/python/cpython/pull/141508
* ``frozendict`` shares most of its code with the ``dict`` type.
* Add ``PyFrozenDictObject`` structure which inherits from
  ``PyDictObject`` and has an additional ``ma_hash`` member.


Thread Safety
=============

Once a ``frozendict`` is created, its shallow immutability is guaranteed.
This means it can be safely shared between threads without synchronization,
as long as its values are not modified by other threads.


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.


Deferred Ideas
==============

New syntax for ``frozendict`` literals
--------------------------------------

Various syntaxes have been proposed to write ``frozendict`` literals.

A new syntax can be added later if needed.

Method to convert ``dict`` to ``frozendict``
--------------------------------------------

Different methods have been proposed to convert a mutable ``dict`` to an
immutable ``frozendict`` with *O*\ (1) complexity, such as
``dict.freeze()``. The idea would be to move ``dict`` contents into
``frozendict``: it would make the ``dict`` empty. Another idea would be
to use "copy-on-write": only copy the ``dict`` at its first
modification.

We consider that such method can be added later if needed, but it
doesn't have to be added right now. Moreover, if such method is added,
it would be nice to add a similar method for ``list``/``tuple`` and
``set``/``frozenset``. See also :pep:`351` (Freeze protocol).

Type annotation
---------------

It `has been proposed
<https://discuss.python.org/t/pep-814-add-frozendict-built-in-type/104854/37>`__
to add ``class TD(TypedDict, frozen=True)`` or ``Frozen[MyTypedDict]``
to define a ``frozendict`` type.

We consider that such type 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.
