Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ...
Expand Down
347 changes: 347 additions & 0 deletions peps/pep-0814.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
PEP: 814
Title: Add frozendict built-in type
Author: Victor Stinner <vstinner@python.org>, Donghee Na <donghee.na@python.org>
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.