6

Suppose I have d = {'dogs': 3}. Using:

d['cats'] = 2 

would create the key 'cats' and give it the value 2.

If I really intend to update a dict with a new key and value, I would use d.update(cats=2) because it feels more explicit.

Having automatic creation of a key feels error prone (especially in larger programs), e.g.:

# I decide to make a change to my dict.
d = {'puppies': 4, 'big_dogs': 2}


# Lots and lots of code.
# ....

def change_my_dogs_to_maximum_room_capacity():
    # But I forgot to change this as well and there is no error to inform me.
    # Instead a bug was created.
    d['dogs'] = 1

Question:
Is there a way to disable the automatic creation of a key that doesn't exist through d[key] = value, and instead raise a KeyError?

Everything else should keep working though:

d = new_dict()                  # Works
d = new_dict(hi=1)              # Works
d.update(c=5, x=2)              # Works
d.setdefault('9', 'something')  # Works

d['a_new_key'] = 1              # Raises KeyError
user
  • 5,370
  • 8
  • 47
  • 75
  • 1
    I guess you could subclass `dict` and write a custom function for the relevant magic method. – TigerhawkT3 Aug 27 '15 at 20:22
  • 1
    Take a look at this http://stackoverflow.com/questions/2390827/how-to-properly-subclass-dict-and-override-getitem-setitem – derricw Aug 27 '15 at 20:24
  • You've contradicted yourself. Why didn't you use `d.update(dogs=1)` like you said you would? – chepner Aug 27 '15 at 20:37
  • @chepner hmm i guess I wasn't clear enough. Because that function doesn't intend to create a new key, rather change the value of the old one. But forgetting to change the function, because of the automatic insertion, would go unnoticed. – user Aug 27 '15 at 20:44
  • Not what you ask for, but if the key names are fixed by the program (not read from a file), consider replacing your dictionary with a custom class. The [`__slots__`](https://docs.python.org/3.4/reference/datamodel.html#slots) declaration fixes the attribute names that can be inserted into objects of this class. – alexis Aug 27 '15 at 21:12

3 Answers3

7

You could create a child of dict with a special __setitem__ method that refuses keys that didn't exist when it was initially created:

class StrictDict(dict):
    def __setitem__(self, key, value):
        if key not in self:
            raise KeyError("{} is not a legal key of this StricDict".format(repr(key)))
        dict.__setitem__(self, key, value)

x = StrictDict({'puppies': 4, 'big_dogs': 2})
x["puppies"] = 23 #this works
x["dogs"] = 42    #this raises an exception

It's not totally bulletproof (it will allow x.update({"cats": 99}) without complaint, for example), but it prevents the most likely case.

Kevin
  • 74,910
  • 12
  • 133
  • 166
  • 2
    @JoranBeasley It Shouldn't™, since it inherits, all other magic methods will stay around. – tew Aug 27 '15 at 21:29
  • ahh it seems your right ... I swear ive done this with strings and it broke all the magic methods that I didnt explicitly subclass ... maybe its just cause string is more primitive than dict or something – Joran Beasley Aug 28 '15 at 17:52
  • This approach provided a good solution to me, which I describe here (too long for StackOverflow): https://persagen.com/2020/03/05/python_dictionaries_default_values_immutable_keys.html – Victoria Stuart Mar 06 '20 at 03:02
0

Inherit dict class and override __setitem__ to suits your needs.Try this

class mydict(dict):
    def __init__(self, *args, **kwargs):
        self.update(*args, **kwargs)
    def __setitem__(self, key, value):
        raise KeyError(key)

>>>a=mydict({'a':3})
>>>d
{'a': 3}
>>>d['a']
3
>>>d['b']=4
KeyError: 'b'
itzMEonTV
  • 19,851
  • 4
  • 39
  • 49
0

This will only allow new keys to be added with key=value using update:

 class MyDict(dict):
    def __init__(self, d):
        dict.__init__(self)
        self.instant = False
        self.update(d)

    def update(self, other=None, **kwargs):
        if other is not None:
            if isinstance(other, dict):
                for k, v in other.items():
                    self[k] = v
            else:
                for k, v in other:
                    self[k] = v
        else:
            dict.update(self, kwargs)
        self.instant = True

    def __setitem__(self, key, value):
        if self.instant and key not in self:
            raise KeyError(key)
        dict.__setitem__(self, key, value)

x = MyDict({1:2,2:3})
x[1] = 100 # works
x.update(cat=1) # works
x.update({2:200}) # works 
x["bar"] = 3 # error
x.update({"foo":2}) # error
x.update([(5,2),(3,4)])  # error
Padraic Cunningham
  • 176,452
  • 29
  • 245
  • 321