23

Is there a way to get a dict comprehension to raise an exception if it would override a key?

For example, I would like the following to error because there are two values for the key 'a':

>>> {k:v for k, v in ('a1', 'a2', 'b3')}
{'a': '2', 'b': '3'}

I realise this can be done with a for loop. Is there a way to do it while keeping the comprehension syntax?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Gary van der Merwe
  • 9,134
  • 3
  • 49
  • 80
  • 2
    Not without additional code. You could explicitly test for the expected length, for example. The dictionary comprehension syntax acts just like a regular dictionary, setting key-value pairs one by one, or the `dict()` callable with a sequence of key-value pairs: last key wins. – Martijn Pieters May 14 '15 at 13:37
  • You could use a side-effect to check whether each key has been seen before, but you can't `raise` from inside a dictionary comprehension. – jonrsharpe May 14 '15 at 13:38
  • Can you `yield` in a dictionary comprehension? Maybe someone else can call `.throw()` and throw an exception into your dict comprehension. But that's probably not very good design. – Kevin May 14 '15 at 13:40
  • 1
    You do **not** want to do this: `dups=set();{k:v for k,v in (...) if (k in dups and error(k) or dups.add(k)) or True}` were `error` is `def error(k): raise ValueError(str(k))`. – Bakuriu May 14 '15 at 13:42
  • @Kevin: a dictionary comprehension is not a generator, no. – Martijn Pieters May 14 '15 at 13:44
  • @MartijnPieters: Well, I was imagining that the comprehension appears inside a generator function... and someone outside the function calls `.throw()`. – Kevin May 14 '15 at 14:00
  • You could be evil. Write a function which you just wrap around your dictionary comprehension. That function will just look at the call stack, find the contents of its own argument list, use `re` to find everything between the `in` and `}` of the dictionary comprehension, then do `len(eval())` on it. Compare that to the length of the object that you're given. If they differ, raise an exception. Posted as a comment just to avoid the downvotes that I know sharing evil code would get me. – ArtOfWarfare May 14 '15 at 14:19
  • @ArtOfWarfare That is not going to work in any meaningful code. If you don't want two identical keys in the list, then you wouldn't write a constant expression which has exactly that. A meaningful use case would be one in which the list is not a constant, and in that case your approach would totally break down. – kasperd May 14 '15 at 15:21
  • @kasperd: Hm. Yeah, you'd need to do more than just an `eval()`... you'd need to also grab all the variables available in that stack frame. – ArtOfWarfare May 14 '15 at 15:36
  • @ArtOfWarfare Even that isn't going to work. The source code is not guaranteed to be available. Even if it is available, there are two other ways it could break: The code may have side effects. The code may not be deterministic. – kasperd May 14 '15 at 15:40
  • I originally set out to write a solution to this question by subclassing `dict`. However, I ended up getting stuck on it, which generated the following question: "[Overriding dict.update() method in subclass to prevent overwriting dict keys](http://stackoverflow.com/q/30241688/1843248)". I'm not going to post the answer as my own work, but the accepted answer to my question is [here](http://stackoverflow.com/a/30242574/1843248) if you're interested in that approach. – Deacon May 15 '15 at 13:16

2 Answers2

18

You can use a generator with a helper function:

class DuplicateKeyError(ValueError): pass

def dict_no_dupl(it):
    d = {}
    for k, v in it:
        if k in d: raise DuplicateKeyError(k)
        d[k] = v
    return d

dict_no_dupl((k, v) for k, v in ('a1', 'a2', 'b3'))

This does add a helper function, but keeps the comprehension syntax (reasonably) intact.

orlp
  • 112,504
  • 36
  • 218
  • 315
14

If you don't care about which key caused a collision:

Check that the generated dict has the appropriate size with len().

Karoly Horvath
  • 94,607
  • 11
  • 117
  • 176