-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
PEP 814: Add frozendict built-in type #4699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
8e55c28
PEP 814: Add frozendict built-in type
vstinner e8afe83
Update CODEOWNERS
vstinner aea7b36
Update .github/CODEOWNERS
vstinner fc4e926
Update peps/pep-0814.rst
vstinner ad1ef65
Apply suggestions from code review
vstinner de14e1a
PEP 814: Rephrase a sentence
vstinner 1685d2f
Address Nathan's review
vstinner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: 06-Nov-2025 | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
| 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 | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
| 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 | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
| results in error. | ||
|
|
||
| * Using an immutable mapping as a function parameter default value | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
| 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 | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
| 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: | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
| * ``frozendict()`` creates a new empty immutable mapping; | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
| * ``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 | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
| keys and values. Pseudo-code of ``hash(frozendict)``:: | ||
|
|
||
| hash(frozenset(frozendict.items())) | ||
|
|
||
| Equality test does not depend on the items order neither. Example:: | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
| >>> 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:: | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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. | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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 | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
| :pep:`779` (Criteria for supported status for free-threaded Python) was | ||
| accepted, people need this for their migration. | ||
|
|
||
|
|
||
| Differences between dict and frozendict | ||
| ======================================= | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
| * ``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 | ||
| ================================================ | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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: | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
| * ``asyncio`` was added in 2014 (Python 3.4) | ||
| * Free Threading was added in 2024 (Python 3.13) | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
| * ``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`` | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
| 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) | ||
| ========== ============= ============== | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| 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 | ||
| ---------------------------------- | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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). | ||
|
vstinner marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| Copyright | ||
| ========= | ||
|
|
||
| This document is placed in the public domain or under the | ||
| CC0-1.0-Universal license, whichever is more permissive. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.