12

Is it possible to "freeze" a python dict after creation so that it's impossible to add new keys to it? It would only be possible to change existing key values.

If not, how do you know when you are changing an existing keyvalue pair, and when you are adding a new one?

danihodovic
  • 1,151
  • 3
  • 18
  • 28
  • You can check to see if a value is in the dictionary with `if(key in dict):...` – flau Aug 11 '14 at 16:29
  • So you want to freeze only the keys, not all the dictionary, right? – enrico.bacis Aug 11 '14 at 16:30
  • 2
    @jonrsharpe -- I'm not sure if I agree with this closing. OP says "frozen", but doesn't really mean it since he/she really just wants to specify the keys which can be manipulated and not allow any others. . . – mgilson Aug 11 '14 at 16:31
  • Ya. But it would be nice to know both – danihodovic Aug 11 '14 at 16:31
  • 1
    Related: http://stackoverflow.com/questions/2703599/what-would-be-a-frozen-dict – jonrsharpe Aug 11 '14 at 16:32
  • 1
    I don't think it's an exact duplicate of that since you want to freeze only the keys. By the way no, in the standard library there isn't such a thing. In order to know if a key is in the dictionary use @iwin suggestion: `print 'already in' if key in dict else 'not here'` – enrico.bacis Aug 11 '14 at 16:34
  • You could also create your own class to do that, or add a decorator to the function to check whether the input is valid (already in the dictionary?). – flau Aug 11 '14 at 16:35
  • @iwin Could you post an example of this if it's not too complicated? – danihodovic Aug 11 '14 at 16:41
  • @poke _This_ question asked to allow "only change existing key values". I think your answer somehow missed this point. – wim Aug 11 '14 at 20:39

2 Answers2

13

Maybe something like this:

class FreezableDict (dict):
    __frozen = False

    def freeze (self):
        self.__frozen = True

    def __setitem__ (self, key, value):
        if self.__frozen and key not in self:
            raise ValueError('Dictionary is frozen')
        super().__setitem__(key, value)
>>> x = FreezableDict({'foo': 'bar', 'baz': 'bla'})
>>> x
{'baz': 'bla', 'foo': 'bar'}
>>> x['asdf'] = 'fdsa'
>>> x
{'asdf': 'fdsa', 'baz': 'bla', 'foo': 'bar'}
>>> x.freeze()
>>> x['hello'] = 'world'
Traceback (most recent call last):
  File "<pyshell#20>", line 1, in <module>
    x['hello'] = 'world'
  File "<pyshell#13>", line 8, in __setitem__
    raise ValueError('Dictionary is frozen')
ValueError: Dictionary is frozen

Note that you might want to overwrite other methods too, including __delitem__, update, setdefault, pop, and popitem, as they can all modify the dictionary.


If you are interested in locking the dictionary completely, you could use types.MappingProxyType which provides a read-only view onto your dictionary. Once you have created your normal dictionary, you can then just create a mapping proxy of it which simply does not have any of the assignment/update functionality. You can also then get rid of any reference to the original dictionary (the mapping will keep one), to prevent it from being used to update it any further:

>>> x = {'foo': 'bar'}
>>> y = types.MappingProxyType(x)
>>> y
mappingproxy({'foo': 'bar'})
>>> x['baz'] = 'bla'
>>> y
mappingproxy({'baz': 'bla', 'foo': 'bar'})
>>> y['hello'] = 'world'
Traceback (most recent call last):
  File "<pyshell#55>", line 1, in <module>
    y['hello'] = 'world'
TypeError: 'mappingproxy' object does not support item assignment
>>> del x
>>> y
mappingproxy({'baz': 'bla', 'foo': 'bar'})

Or just in a single line, without ever having a reference to the original dictionary:

>>> x = types.MappingProxyType({'foo': 'bar', 'baz': 'bla'})
>>> x
mappingproxy({'baz': 'bla', 'foo': 'bar'})
>>> x['hello'] = 'world'
Traceback (most recent call last):
  File "<pyshell#60>", line 1, in <module>
    x['hello'] = 'world'
TypeError: 'mappingproxy' object does not support item assignment
poke
  • 369,085
  • 72
  • 557
  • 602
4

This isn't possible with a "vanilla" dict. You'll probably want to subclass collections.MutableMapping . . .

Untested code follows

class FrozenKeyDict(collections.MutableMapping):
    """Mapping which doesn't allow keys to be added/deleted.

    It does allow existing key/value pairs to be modified.
    """
    def __init__(self, *args, **kwargs):
        self._frozen = False
        self._dict = {}
        super(FrozenKeyDict, self).__init__(*args, **kwargs)
        self._frozen = True

    def __getitem__(self, key):
        return self._dict[key]

    def __setitem__(self, key, value):
        if self._frozen and key not in self._dict:
            raise KeyError('must be one of %s' % list(self))
        self._dict[key] = value

    def __delitem__(self, key):
        # modify to suit your needs ...
        raise KeyError('Removing keys not supported')

    def __iter__(self):
        return iter(self._dict)

    def __len__(self):
        return len(self._dict)
mgilson
  • 300,191
  • 65
  • 633
  • 696
  • Your `super` and class name don't seem consistent :) – Jon Clements Aug 11 '14 at 17:16
  • @JonClements -- D'oh! that's what happens when you can't decide how you want to name things and aren't using python3.x for your example ;-) – mgilson Aug 11 '14 at 17:17
  • I'd also probably change `self._dict.keys()` to be `list(self._dict)` so it's 3.x compatible as well – Jon Clements Aug 11 '14 at 17:19
  • @JonClements -- I go back and forth on that one -- People often expect `list(self._dict)` to list the `items`, not just the keys. Maybe `list(self)` to force a little bit of thought? – mgilson Aug 11 '14 at 17:24
  • ummm, `list(self)` will be empty though as all the data is in the `self._dict` not the `collections.MutableMapping` part of the object (pick one or the other) – Jon Clements Aug 11 '14 at 17:28
  • @JonClements -- `__iter__(self)` just returns `iter(self._dict)`, so `list(self._dict)` and `list(self)` should always return the same thing. no? (`__iter__` is a required part of the `MutableMapping` interface) We could even use that idea in the membership test above -- `if self._frozen and key not in self`, but I figured that `dict.__contains__` is probably a bit more efficient than `MutableMapping.__contains__` as the latter relies heavily on exception handling. – mgilson Aug 11 '14 at 17:31
  • Ahhh yes... very good point... I completely missed the `__iter__` - I retract my comment :) – Jon Clements Aug 11 '14 at 17:32
  • But, maybe that means that `list(self)` requires TOO much thought (if it threw _you_ for a loop ...) *sigh* – mgilson Aug 11 '14 at 17:33
  • I'm sure if I'd have done more than take a cursory glance then I'd have seen the connection (silly me). To a casual observer it just looks wrong... although, it's absolutely fine, and relying on `__iter__` makes sense in case yielding anything different from the keys is required - keeps it all in one place... But then, on the other hand, the error is specifically based on the restriction of keys, so a different `__iter__` might make that error message meaningless... – Jon Clements Aug 11 '14 at 17:39
  • Yeah, I'm not sure ... I think I'm just going to leave it. The idea was to show how to create a mapping which has an immutable set of keys. Spending too much of our collective brainpower figuring out the best way to format the exception message probably isn't worth it :-) – mgilson Aug 11 '14 at 17:41