PEP 814: Add frozendict built-in type

frozendict implements the Mapping ABC and has almost the same interface as dict (differences with dict are listed in the dedicated section). dict is a superset of Mapping. The interface is defined by the Specification section.

frozendict has no __ior__() (a |= b) method, but implements __or__() (a | b). Python implements a |= b for frozendict. It’s the same as:

class MyClass:
    def __init__(self, value):
        self.value = value
    def __or__(self, other):
        return MyClass(self.value | other)

a = MyClass(1)
a |= 2
print(a.value)  # output: 3

frozendict[str, list[int]] is a valid frozendict. Why would it raise an exception? Or do you expect a linter to emit a warning/error on hash(x)?

It can be easily added after PEP 814 implementation. IMO the PEP doesn’t have to cover the full scope of all possible frozendict usages. typing.TypedDict was already discussed earlier.

3 Likes

I get understand the motivation to be consistent with the language and frozenset and friends. The language has evolved and we don’t really use += on a str or list anymore, and people are often confused or surprised when they see += on a tuple. The |= seems like it’s in the same boat.

I’m talking about in the context of mypy and other static analysis tools.

I agree. There’s a lot of typing leaning Python users now.

The language still provides for executing |= by using __or__ and an assignment (__ior__ is just the ability to optimise, it isn’t the only way the operation exists). If you’re proposing to remove this from the language, this is definitely not the right PEP or thread.

Victor’s point is that |= works naturally because of the language. The PEP doesn’t have to indicate this one way or the other - those who know how augmented assignments work will understand, those who don’t will learn something, and those who will evaluate the PEP are (hopefully) drawn from the first group.

4 Likes

But we use it on int. And the exact same mechanisms that make it work on int make it work on str, tuple and |= with frozendict. [1]

If you genuinely think this is a footgun, make a new thread. But be prepare for that to be a massive uphill battle.

In general, I would suggest you make sure you understand how and why the language works before making sweeping claims, especially when using language like “we don’t” implying you speak for the community when you may not be doing that.


  1. Note that list is not part of this of …list because it actually has a difference between a += b and a = a + b ↩︎

6 Likes

My apologies. My intent wasn’t to derail the discussion.

2 Likes

Speaking as a person who has implemented TypedDict features in type checkers: There’s enough specialized knowledge§ to get type system changes specified (and implemented in type checkers) that I’d advocate keeping any TypedFrozenDict out of the scope of the initial PEP.

§ There’s different specialized knowledge to get a built in type like frozendict landed in CPython

10 Likes

We submitted PEP 814 to the Steering Council! Thanks everyone for this constructive discussion! We tried to address all feedback in PEP 814. For example, a Type annotation section has been added.

33 Likes

I’m excited for this and have some clarifying questions for the PEP.

The PEP mentions:

Using an immutable mapping as a function parameter’s default value avoids the problem of mutable default values.

Could an example be given to make it more clear how and when frozendict should be used to obtain these benefits?

If I understand correctly, previously one could erroneously write:

def f(key, value, d={}):
    d[key] = value
    return d

f("a", 1) # {"a": 1}
f("b", 2) # {"a": 1, "b": 2}

And thus would instead have to write:

def f(key, value, d=None):
    if d is None:
       d = {}
    d[key] = value
    return d

f("a", 1) # {"a": 1}
f("b", 2) # {"b": 2}

As I understand, with frozendicts, the idea would be:

def f(key, value, d=frozendict()):
    d[key] = value # error
    return d

But this would fail for trying to update an immutable.

So the expected case is instead:

def f(key, value, d=frozendict()):
    d |= {key: value} # O(n)
    return d

f("a", 1) # {"a": 1} (frozendict)
f("b", 2) # {"b": 2} (frozendict)

With the caveat being that this and all future updates are O(n)?

And thus to avoid this, would the following need to be written instead?

def f(key, value, d=frozendict()):
    d = dict(d) # O(n)
    d[key] = value # not O(n)
    return d

f("a", 1) # {"a": 1} (dict)
f("b", 2) # {"b": 2} (dict)

Am I understanding the use case correctly to not have a way to replace the if d is None syntax without an O(n) slowdown?

1 Like

For now, it seems so.

This is the opposite issue - turning a frozendict → dict, but it’s a similar case.

So at some point, there may be a way - but this PEP is really focused on getting the feature into the language first :slight_smile:

1 Like

Yes but your example is not typical:

What is this function? What name would you give to it rather than f that would describe what it does? Something like set_key_in_dict_or_make_new_dict?

The function mutates a dict that was passed in but the argument to be mutated is optional and if you don’t pass it then it creates a different dict and returns that? Why does the function both mutate its argument and return the mutated result?

The more common case here is that you don’t really want to mutate the argument whether passed in or using the default value. Although you don’t intend to mutate it you don’t like risking the possibility that some unthinking code added in future might mutate it or that the function might return a reference that some other code mutates.

Seeing that the default value is immutable immediately reassures that obvious mistakes that could mutate it will not happen without needing to trace all of the rest of the code to verify that nothing mutates it or leaks a reference to it.

4 Likes

Correct.

The use case from the PEP is the if d is None one, but when the code already doesn’t mutate d. It’s more of a safety mechanism so you don’t accidently mutate d (which is typically a bug that’s hard to notice if you use a dict as the default value).

2 Likes

Yes but your example is not typical …

What is this function? …

Apologies for not specifying, the example was purely hypothetical to match what is typically seen for explaining how mutable arguments can go wrong.

The questions raised regardless are quite valid!

you don’t really want to mutate the argument whether passed in or using the default value.

Agree, hence it may be helpful to provide examples of how best to use frozendict for mutable defaults.

Kind of, but with one nuance - it is not frozendict’s job to do what dict does.

Object is either mutable or not and somehow you expect the object to be in a quantum state of being both at the same time.


The use case for it in defaults is the same as the one for tuple.

def foo(these_are_ok=('a', 'b')):
    # I need to extend once
    these_are_ok += ('c',)
    # Or if many times
    these_are_ok = list(these_are_ok)
    these_are_ok.append
    these_are_ok.append
    these_are_ok.append

The point here is to have a default which can not be modified.

If you need to extend it, you find the solution that serves your case best.

In short, you can draw examples and best practices from tuple default cases.


Now this is regarding convenience.
But as you have noted complexities, I suppose you are worried about performance as well.

On this I can not comment much as I haven’t investigated it.
All I know is that it hasn’t become an issue for tuple despite long practice of such use.

Same might not apply to frozendict.
But if does need addressing, it is unlikely to be worth breaking any semantics for it.

I think your input with real life use cases where you are concerned with performance would be very helpful in this regard.

As far as I have seen, there are ideas for optimizations, but I think all input is valuable to determine the order of importance.

From the PEP:

Keys must be hashable and therefore immutable, but values can be mutable.

I think someone mentioned this above, but it’s worth repeating: hashable doesn’t imply immutable.

Using immutable values creates a hashable frozendict.

I don’t think immutable implies hashable either. Perhaps these two sentences should be re-written to avoid referring to mutability at all, something like:

Keys must be hashable, but values need not be. Using hashable values creates a hashable frozendict.

1 Like