Skip to content

Commit 3ede647

Browse files
vstinnerhugovk
andauthored
PEP 814: Add frozendict built-in type (#4699)
Co-authored-by: Hugo van Kemenade <[email protected]>
1 parent b6bc205 commit 3ede647

File tree

2 files changed

+348
-0
lines changed

2 files changed

+348
-0
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,7 @@ peps/pep-0807.rst @dstufft
687687
peps/pep-0809.rst @zooba
688688
peps/pep-0810.rst @pablogsal @DinoV @Yhg1s
689689
peps/pep-0811.rst @sethmlarson @gpshead
690+
peps/pep-0814.rst @vstinner @corona10
690691
# ...
691692
peps/pep-2026.rst @hugovk
692693
# ...

peps/pep-0814.rst

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
PEP: 814
2+
Title: Add frozendict built-in type
3+
Author: Victor Stinner <[email protected]>, Donghee Na <[email protected]>
4+
Status: Draft
5+
Type: Standards Track
6+
Created: 12-Nov-2025
7+
Python-Version: 3.15
8+
9+
Abstract
10+
========
11+
12+
A new public immutable type ``frozendict`` is added to the ``builtins``
13+
module.
14+
15+
We expect ``frozendict`` to be safe by design, as it prevents any unintended
16+
modifications. This addition benefits not only CPython’s standard
17+
library, but also third-party maintainers who can take advantage of a
18+
reliable, immutable dictionary type.
19+
20+
21+
Rationale
22+
=========
23+
24+
The proposed ``frozendict`` type:
25+
26+
* implements the ``collections.abc.Mapping`` protocol,
27+
* supports pickling.
28+
29+
The following use cases illustrate why an immutable mapping is
30+
desirable:
31+
32+
* Immutable mappings are hashable which allows their use as dictionary
33+
keys or set elements.
34+
35+
* This hashable property permits functions decorated with
36+
``@functools.lru_cache()`` to accept immutable mappings as arguments.
37+
Unlike an immutable mapping, passing a plain ``dict`` to such a function
38+
results in error.
39+
40+
* Using an immutable mapping as a function parameter's default value
41+
avoids the problem of mutable default values.
42+
43+
* Immutable mappings can be used to safely share dictionaries across
44+
thread and asynchronous task boundaries. The immutability makes it
45+
easier to reason about threads and asynchronous tasks.
46+
47+
There are already third-party ``frozendict`` and ``frozenmap`` packages
48+
available on PyPI, proving that there is demand for
49+
immutable mappings.
50+
51+
52+
Specification
53+
=============
54+
55+
A new public immutable type ``frozendict`` is added to the ``builtins``
56+
module. It is not a ``dict`` subclass but inherits directly from
57+
``object``.
58+
59+
60+
Construction
61+
------------
62+
63+
``frozendict`` implements a ``dict``-like construction API:
64+
65+
* ``frozendict()`` creates a new empty immutable mapping.
66+
67+
* ``frozendict(**kwargs)`` creates a mapping from ``**kwargs``,
68+
e.g. ``frozendict(x=1, y=2)``.
69+
70+
* ``frozendict(collection)`` creates a mapping from the passed
71+
collection object. The passed collection object can be:
72+
73+
- a ``dict``,
74+
- another ``frozendict``,
75+
- or an iterable of key/value tuples.
76+
77+
The insertion order is preserved.
78+
79+
80+
Iteration
81+
---------
82+
83+
As ``frozendict`` implements the standard ``collections.abc.Mapping``
84+
protocol, so all expected methods of iteration are supported::
85+
86+
assert list(m.items()) == [('foo', 'bar')]
87+
assert list(m.keys()) == ['foo']
88+
assert list(m.values()) == ['bar']
89+
assert list(m) == ['foo']
90+
91+
Iterating on ``frozendict``, as on ``dict``, uses the insertion order.
92+
93+
94+
Hashing
95+
-------
96+
97+
``frozendict`` instances can be hashable just like tuple objects::
98+
99+
hash(frozendict(foo='bar')) # works
100+
hash(frozendict(foo=['a', 'b', 'c'])) # error, list is not hashable
101+
102+
The hash value does not depend on the items' order. It is computed on
103+
keys and values. Pseudo-code of ``hash(frozendict)``::
104+
105+
hash(frozenset(frozendict.items()))
106+
107+
Equality test does not depend on the items' order either. Example::
108+
109+
>>> a = frozendict(x=1, y=2)
110+
>>> b = frozendict(y=2, x=1)
111+
>>> hash(a) == hash(b)
112+
True
113+
>>> a == b
114+
True
115+
116+
117+
Typing
118+
------
119+
120+
It is possible to use the standard typing notation for ``frozendict``\ s::
121+
122+
m: frozendict[str, int] = frozendict(x=1)
123+
124+
125+
Representation
126+
--------------
127+
128+
``frozendict`` will not use a special syntax for its representation.
129+
The ``repr()`` of a ``frozendict`` instance looks like this:
130+
131+
>>> frozendict(x=1, y=2)
132+
frozendict({'x': 1, 'y': 2})
133+
134+
135+
C API
136+
-----
137+
138+
Add the following APIs:
139+
140+
* ``PyFrozenDict_Type``
141+
* ``PyFrozenDict_New(collection)`` function
142+
* ``PyFrozenDict_Check()`` macro
143+
* ``PyFrozenDict_CheckExact()`` macro
144+
145+
Even if ``frozendict`` is not a ``dict`` subclass, it can be used with
146+
``PyDict_GetItemRef()`` and similar "PyDict_Get" functions.
147+
148+
Passing a ``frozendict`` to ``PyDict_SetItem()`` or ``PyDict_DelItem()``
149+
fails with ``TypeError``. ``PyDict_Check()`` on a ``frozendict`` is
150+
false.
151+
152+
Exposing the C API helps authors of C extensions supporting
153+
``frozendict`` when they need to support thread-safe immutable
154+
containers. It will be important since
155+
:pep:`779` (Criteria for supported status for free-threaded Python) was
156+
accepted, people need this for their migration.
157+
158+
159+
Differences between ``dict`` and ``frozendict``
160+
===============================================
161+
162+
* ``dict`` has more methods than ``frozendict``:
163+
164+
* ``__delitem__(key)``
165+
* ``__setitem__(key, value)``
166+
* ``clear()``
167+
* ``pop(key)``
168+
* ``popitem()``
169+
* ``setdefault(key, value)``
170+
* ``update(*args, **kwargs)``
171+
172+
* A ``frozendict`` can be hashed with ``hash(frozendict)`` if all keys
173+
and values can be hashed.
174+
175+
176+
Possible candidates for ``frozendict`` in the stdlib
177+
====================================================
178+
179+
We have identified several stdlib modules where adopting ``frozendict``
180+
can enhance safety and prevent unintended modifications by design. We
181+
also believe that there are additional potential use cases beyond the
182+
ones listed below.
183+
184+
Note: it remains possible to bind again a variable to a new modified
185+
``frozendict`` or a new mutable ``dict``.
186+
187+
Python modules
188+
--------------
189+
190+
Replace ``dict`` with ``frozendict`` in function results:
191+
192+
* ``email.headerregistry``: ``ParameterizedMIMEHeader.params()``
193+
(replace ``MappingProxyType``)
194+
* ``enum``: ``EnumType.__members__()`` (replace ``MappingProxyType``)
195+
196+
Replace ``dict`` with ``frozendict`` for constants:
197+
198+
* ``_opcode_metadata``: ``_specializations``, ``_specialized_opmap``,
199+
``opmap``
200+
* ``_pydatetime``: ``specs`` (in ``_format_time()``)
201+
* ``_pydecimal``: ``_condition_map``
202+
* ``bdb``: ``_MonitoringTracer.EVENT_CALLBACK_MAP``
203+
* ``dataclasses``: ``_hash_action``
204+
* ``dis``: ``deoptmap``, ``COMPILER_FLAG_NAMES``
205+
* ``functools``: ``_convert``
206+
* ``gettext``: ``_binary_ops``, ``_c2py_ops``
207+
* ``imaplib``: ``Commands``, ``Mon2num``
208+
* ``json.decoder``: ``_CONSTANTS``, ``BACKSLASH``
209+
* ``json.encoder``: ``ESCAPE_DCT``
210+
* ``json.tool``: ``_group_to_theme_color``
211+
* ``locale``: ``locale_encoding_alias``, ``locale_alias``,
212+
``windows_locale``
213+
* ``opcode``: ``_cache_format``, ``_inline_cache_entries``
214+
* ``optparse``: ``_builtin_cvt``
215+
* ``platform``: ``_ver_stages``, ``_default_architecture``
216+
* ``plistlib``: ``_BINARY_FORMAT``
217+
* ``ssl``: ``_PROTOCOL_NAMES``
218+
* ``stringprep``: ``b3_exceptions``
219+
* ``symtable``: ``_scopes_value_to_name``
220+
* ``tarfile``: ``PAX_NUMBER_FIELDS``, ``_NAMED_FILTERS``
221+
* ``token``: ``tok_name``, ``EXACT_TOKEN_TYPES``
222+
* ``tomllib._parser``: ``BASIC_STR_ESCAPE_REPLACEMENTS``
223+
* ``typing``: ``_PROTO_ALLOWLIST``
224+
225+
Extension modules
226+
-----------------
227+
228+
Replace ``dict`` with ``frozendict`` for constants:
229+
230+
* ``errno``: ``errorcode``
231+
232+
233+
Relationship to PEP 416 frozendict
234+
==================================
235+
236+
Since 2012 (:pep:`416`), the Python ecosystem has evolved:
237+
238+
* ``asyncio`` was added in 2014 (Python 3.4)
239+
* Free threading was added in 2024 (Python 3.13)
240+
* ``concurrent.interpreters`` was added in 2025 (Python 3.14)
241+
242+
There are now more use cases to share immutable mappings.
243+
244+
``frozendict`` now preserves the insertion order, whereas PEP 416
245+
``frozendict`` was unordered (as :pep:`603` ``frozenmap``). ``frozendict``
246+
relies on the ``dict`` implementation which preserves the insertion
247+
order since Python 3.6.
248+
249+
The first motivation to add ``frozendict`` was to implement a sandbox
250+
in Python. It's no longer the case in this PEP.
251+
252+
``types.MappingProxyType`` was added in 2012 (Python 3.3). This type is
253+
not hashable and it's not possible to inherit from it. It's also easy to
254+
retrieve the original dictionary which can be mutated, for example using
255+
``gc.get_referents()``.
256+
257+
258+
Relationship to PEP 603 frozenmap
259+
=================================
260+
261+
``collections.frozenmap`` has different properties than frozendict:
262+
263+
* ``frozenmap`` items are unordered, whereas ``frozendict`` preserves
264+
the insertion order.
265+
* ``frozenmap`` has additional methods:
266+
267+
* ``including(key, value)``
268+
* ``excluding(key)``
269+
* ``union(mapping=None, **kw)``
270+
271+
========== ============== ==============
272+
Complexity ``frozenmap`` ``frozendict``
273+
========== ============== ==============
274+
Lookup *O*\ (log *n*) *O*\ (1)
275+
Copy *O*\ (1) *O*\ (*n*)
276+
========== ============== ==============
277+
278+
279+
Reference Implementation
280+
========================
281+
282+
* The reference implementation is still a work-in-progress.
283+
* ``frozendict`` shares most of its code with the ``dict`` type.
284+
* Add ``PyFrozenDictObject`` which inherits from ``PyDictObject`` and
285+
has an additional ``ma_hash`` member.
286+
287+
288+
Thread Safety
289+
=============
290+
291+
Once the ``frozendict`` is created, it is immutable and can be shared
292+
safely between threads without any synchronization.
293+
294+
295+
Future Work
296+
===========
297+
298+
We are also going to make ``frozendict`` to be more efficient in terms
299+
of memory usage and performance compared to ``dict`` in future.
300+
301+
302+
Rejected Ideas
303+
==============
304+
305+
Inherit from dict
306+
-----------------
307+
308+
If ``frozendict`` inherits from ``dict``, it would become possible to
309+
call ``dict`` methods to mutate an immutable ``frozendict``. For
310+
example, it would be possible to call
311+
``dict.__setitem__(frozendict, key, value)``.
312+
313+
It may be possible to prevent modifying ``frozendict`` using ``dict``
314+
methods, but that would require to explicitly exclude ``frozendict``
315+
which can affect ``dict`` performance. Also, there is a higher risk of
316+
forgetting to exclude ``frozendict`` in some methods.
317+
318+
If ``frozendict`` does not inherit from ``dict``, there is no such
319+
issue.
320+
321+
322+
New syntax for ``frozendict`` literals
323+
--------------------------------------
324+
325+
Various syntaxes have been proposed to write ``frozendict`` literals.
326+
327+
A new syntax can be added later if needed.
328+
329+
330+
References
331+
==========
332+
333+
* :pep:`416` (``frozendict``)
334+
* :pep:`603` (``collections.frozenmap``)
335+
336+
337+
Acknowledgements
338+
================
339+
340+
This PEP is based on prior work from Yury Selivanov (:pep:`603`).
341+
342+
343+
Copyright
344+
=========
345+
346+
This document is placed in the public domain or under the
347+
CC0-1.0-Universal license, whichever is more permissive.

0 commit comments

Comments
 (0)