PEP 814: Add frozendict built-in type

:partying_face:

So the JIT will not make any optimization for immutable types?

I didn’t get it. As far as I understood, if a frozendict is hashable, it means all its values are hashable too, like tuple. This usually[1] means all keys and values are immutable. So if hashability is a focus, why not thread safety?


  1. this is not always true of course. See instances of custom classes for example ↩︎

That is not what the quote says. The quote says the PEP should not use performance as a motivation - independent of the PEP, performance improvements can of course be made where sensible.

I hope noone is argue for tuples because of thread saftey - this is just as incorrect of an assumption there as it is here. (For that you need something closer to PEP 795).

But again. The point is that the PEP shouldn’t exaggerate the guarantees - it may result in an incorrect perception by users that this data structure promises things it doesn’t and can’t.

Note that with maybe the exception of the last point, none of the things listed as requested by the SC are technical changes - they are all just requests to update docs to curb expectations.

10 Likes

I see. I took a look to the PEP, and indeed it says:

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.

It’s a pity if it will be a non-goal. tuple is more compact than list. Personally, when I use it explicitly, it’s mainly for this reason.

I agree that if you create a frozendict with mutable values it’s not thread safe, but if the values are (deeply) immutables too, it is.

In short, a frozendict is thread safe if it’s deeply immutable, as a tuple. In this case, it also has a hash. Maybe the PEP should mention that frozendict alone doesn’t make the map automagically immutable.

About PEP 795, I think it’s something apart. If I understood it well, it’s purpose is to avoid pickling, that would be great. But it seems to me that this PEP merely says that you can use frozendict without guards == that’s partly true, as we saw.

Coding ergonomics takes precedence in Python as the SC pointed out.

At this point I’m going to say as an admin that future discussions regarding perf are off-topic and should be a separate topic once the code lands in main.

17 Likes

The frozendict built-in type is now implemented in Python 3.15 (documentation). Multiple modules have already been updated to support this new type (json, pickle, pprint, etc.).

There is still an on-going work to support frozendict in more stdlib modules, and maybe also replace dict with frozendict where it is relevant. There is also an on-going work on optimizing frozendict.

You can now play with frozendict in the main branch. The future Python 3.15.0 alpha 7 release (2026-03-10) will include it.

30 Likes

Since frozendict will not inherit from dict will instance checks such as:

my_frozen_dict = frozendict(a=1, b=2, c=3)
isintance(my_frozen_dict, dict)
match my_frozen_dict:
    case dict():
        ...

work?

Wouldn’t it be better to check against the Mapping protocol in that instance? Since the interfaces differ, you wouldn’t really want to handle a frozendict and a dict the same way.

3 Likes

They will work in that they will not match a frozendict. If they did, then they would be broken as a frozendict does not support the same methods as dict.

>>> example = frozendict(a=42)
>>> example
frozendict({'a': 42})
>>> isinstance(example, dict)
False
>>> isinstance(example, frozendict)
True
>>> match example:
...     case dict():
...         print("dict")
...     case frozendict():
...         print("frozendict")
...     case _:
...         print("neither")
...         
frozendict
2 Likes

I have read the PEP and did a quick search on DPO for this, sorry if I missed something.

Has there been any discussion about having a backport for pre-3.15 releases?
I imagine something similar to what was done with backports.zstd, allowing library authors to integrate frozendict behind a guard on sys.version.

13 Likes

Perhaps an admin can split this into a new topic for visibility? Otherwise I can just create a new thread.

There is already the frozendict project on PyPI which works on Python 3.14 and older. It’s similar to Python 3.15 built-in frozendict (PEP 814):

  • Similar constructor and iterator APIs.
  • Preserve insertion order.
  • Can be hashed if all keys and values can be hashed.
  • Comparison doesn’t take insertion order in account.

So it’s a good solution to use frozendict on Python 3.14 and older.

But there are also differences:

  • It lives in the frozendict module, instead of builtins.
  • It inherits from dict, instead of object.
  • It’s less well integrated into the stdlib. For example, the pprint module doesn’t support it (but copy, json and pickle work well).
  • It has dict methods to modify a dictionary (clear(), __setitem__(), pop(), etc.), but these methods raise exceptions.
  • There is no C API to accept frozendict.

There are ways to modify a frozendict.frozendict. Using dict methods (parent class):

>>> fd=frozendict.frozendict(x=1)
>>> dict.__init__(fd, y=2)
>>> fd
frozendict.frozendict({'x': 1, 'y': 2})
>>> dict.__setitem__(fd, 'x', 2)
>>> fd
frozendict.frozendict({'x': 2, 'y': 2})
>>> dict.clear(fd)
>>> fd
frozendict.frozendict({})

Or using a function which calls PyDict_SetItem() C API:

>>> fd=frozendict.frozendict(x=1)
>>> import _testlimitedcapi
>>> _testlimitedcapi.dict_setitem(fd, "y", 2)
0
>>> fd
frozendict.frozendict({'x': 1, 'y': 2})
5 Likes

I just want to point out here, for future reference, that if a frozen typed dict is added, then (example modified from Required/NotRequired and inheritance · python/typing · Discussion #1516 · GitHub)

from typing import NotRequired, Required, TypedDict

class A(TypedDict, frozen=True):
    a: NotRequired[int]
    b: Required[int]

class B(A, frozen=True):
    a: Required[int]

`B` would be a valid subtype of `A` (it would not be if they were plain `TypedDict`s). The main LSP reason (there were also PEP wording reasons) for disallowing this before was you could delete the `a` key from an `A` object, but not a `B`. Since deleting a key is disallowed for a frozen typed dict, this would no longer be an issue.

Edit: Fixed formatting

2 Likes

In current main, frozendict always creates a new object. Isn’t that wasteful?

>>> d = frozendict(a=1, b=2)
>>> frozendict(d) is d
False

Other immutable types in the stdlib don’t do this:

>>> t = (1, 2)
>>> tuple(t) is t
True

>>> s = 'abcdef'
>>> str(s) is s
True

>>> f = 3.14159
>>> float(f) is f
True
3 Likes

frozenset also returns a new object every time:

>>> frozenset([1, 2]) is frozenset([1, 2])
False

And so does tuple if it’s not made purely of literals:

>>> a = 1
>>> (a, 1) is (a, 1)
False

Maybe all of these could be changed, but I don’t think this is a clear defect in frozendict.

I was referring to the __new__ behaviour – calling the type.

>>> fs = frozenset([1, 2])
>>> frozenset(fs) is fs
True

>>> a = 1
>>> t = (a, 1)
>>> tuple(t) is t
True
3 Likes

Ah, that’s a missed optimizations. There are likely a few more which have not been spotted yet :slight_smile: I wrote PR gh-145592 to implement this optimization.

9 Likes

Since I used it in past, I remembered that it was “really” immutable. It seems you’re right for Python 3.11+. For previous versions, it seems it uses a C extension.