134

Is there any straightforward way of finding a key by knowing the value within a dictionary?

All I can think of is this:

key = [key for key, value in dict_obj.items() if value == 'value'][0]
vaultah
  • 44,105
  • 12
  • 114
  • 143
RadiantHex
  • 24,907
  • 47
  • 148
  • 244
  • possible duplicate: http://stackoverflow.com/questions/483666/python-reverse-inverse-a-mapping – Tobias Kienzler May 28 '13 at 13:26
  • Google guided me here... And i must say.. why is no one using `iteritems` as for me this makes a 40x faster difference... using the ().next method – Angry 84 Oct 19 '16 at 05:56
  • 7
    If you have a lot of reverse lookups to do: `reverse_dictionary = {v:k for k,v in dictionary.items()}` – Austin Jul 06 '18 at 21:33
  • @TobiasKienzler while the problem certainly *could* be solved by inverting the dictionary and then doing a normal lookup, the apparent goal here is simply to determine which key corresponds to a certain value. There are other approaches to this, although they will still be O(N). – Karl Knechtel May 12 '23 at 04:56

13 Answers13

129

Your list comprehension goes through all the dict's items finding all the matches, then just returns the first key. This generator expression will only iterate as far as necessary to return the first value:

key = next(key for key, value in dd.items() if value == 'value')

where dd is the dict. Will raise StopIteration if no match is found, so you might want to catch that and return a more appropriate exception like ValueError or KeyError.

vaultah
  • 44,105
  • 12
  • 114
  • 143
PaulMcG
  • 62,419
  • 16
  • 94
  • 130
  • 2
    Yes It should probably raise same exception as listObject.index(key) when key is not in the list. – Brian Jack Jul 25 '12 at 19:48
  • 7
    also `keys = { key for key,value in dd.items() if value=='value' }` to get the set of all keys if several matches. – askewchan May 16 '13 at 19:57
  • 6
    @askewchan - no real need to return this as a set, dict keys already have to be unique, just return a list - or better, return a generator expression, and let the caller put it in whatever container they want. – PaulMcG May 19 '13 at 16:17
68

There are cases where a dictionary is a one:one mapping

Eg,

d = {1: "one", 2: "two" ...}

Your approach is ok if you are only doing a single lookup. However if you need to do more than one lookup it will be more efficient to create an inverse dictionary

ivd = {v: k for k, v in d.items()}

If there is a possibility of multiple keys with the same value, you will need to specify the desired behaviour in this case.

If your Python is 2.6 or older, you can use

ivd = dict((v, k) for k, v in d.items())
John La Rooy
  • 295,403
  • 53
  • 369
  • 502
  • 7
    Nice optimization. But, I think you meant to turn your list of 2-tuples into a dictionary using dict(): `ivd=dict([(v,k) for (k,v) in d.items()])` – hobs Jul 25 '12 at 20:44
  • 4
    @hobs just use a dict comprehension instead of list comprehension: `invd = { v:k for k,v in d.items() }` – askewchan May 16 '13 at 19:50
  • @gnibbler dict comprehensions haven't been migrated back to Python 2.6, so if you want to remain portable you'll need to put up with the 6 extra characters for dict() around a generator of 2-tuples or a list comprehension of 2-tuples – hobs May 16 '13 at 22:23
  • @hobs, I added that to my answer. – John La Rooy May 17 '13 at 02:19
33

This version is 26% shorter than yours but functions identically, even for redundant/ambiguous values (returns the first match, as yours does). However, it is probably twice as slow as yours, because it creates a list from the dict twice.

key = dict_obj.keys()[dict_obj.values().index(value)]

Or if you prefer brevity over readability you can save one more character with

key = list(dict_obj)[dict_obj.values().index(value)]

And if you prefer efficiency, @PaulMcGuire's approach is better. If there are lots of keys that share the same value it's more efficient not to instantiate that list of keys with a list comprehension and instead use use a generator:

key = (key for key, value in dict_obj.items() if value == 'value').next()
Community
  • 1
  • 1
hobs
  • 18,473
  • 10
  • 83
  • 106
  • 2
    Assuming an atomic operation, are keys and values guaranteed to be in the same corresponding order? – Noctis Skytower May 25 '16 at 14:09
  • 1
    @NoctisSkytower Yes, `dict.keys()` and `dict.values()` are guaranteed to correspond as long as the `dict` isn't mutated between calls. – hobs May 25 '16 at 16:07
8

Since this is still very relevant, the first Google hit and I just spend some time figuring this out, I'll post my (working in Python 3) solution:

testdict = {'one'   : '1',
            'two'   : '2',
            'three' : '3',
            'four'  : '4'
            }

value = '2'

[key for key in testdict.items() if key[1] == value][0][0]

Out[1]: 'two'

It will give you the first value that matches.

Freek
  • 1,097
  • 2
  • 12
  • 30
6

Maybe a dictionary-like class such as DoubleDict down below is what you want? You can use any one of the provided metaclasses in conjuction with DoubleDict or may avoid using any metaclass at all.

import functools
import threading

################################################################################

class _DDChecker(type):

    def __new__(cls, name, bases, classdict):
        for key, value in classdict.items():
            if key not in {'__new__', '__slots__', '_DoubleDict__dict_view'}:
                classdict[key] = cls._wrap(value)
        return super().__new__(cls, name, bases, classdict)

    @staticmethod
    def _wrap(function):
        @functools.wraps(function)
        def check(self, *args, **kwargs):
            value = function(self, *args, **kwargs)
            if self._DoubleDict__forward != \
               dict(map(reversed, self._DoubleDict__reverse.items())):
                raise RuntimeError('Forward & Reverse are not equivalent!')
            return value
        return check

################################################################################

class _DDAtomic(_DDChecker):

    def __new__(cls, name, bases, classdict):
        if not bases:
            classdict['__slots__'] += ('_DDAtomic__mutex',)
            classdict['__new__'] = cls._atomic_new
        return super().__new__(cls, name, bases, classdict)

    @staticmethod
    def _atomic_new(cls, iterable=(), **pairs):
        instance = object.__new__(cls, iterable, **pairs)
        instance.__mutex = threading.RLock()
        instance.clear()
        return instance

    @staticmethod
    def _wrap(function):
        @functools.wraps(function)
        def atomic(self, *args, **kwargs):
            with self.__mutex:
                return function(self, *args, **kwargs)
        return atomic

################################################################################

class _DDAtomicChecker(_DDAtomic):

    @staticmethod
    def _wrap(function):
        return _DDAtomic._wrap(_DDChecker._wrap(function))

################################################################################

class DoubleDict(metaclass=_DDAtomicChecker):

    __slots__ = '__forward', '__reverse'

    def __new__(cls, iterable=(), **pairs):
        instance = super().__new__(cls, iterable, **pairs)
        instance.clear()
        return instance

    def __init__(self, iterable=(), **pairs):
        self.update(iterable, **pairs)

    ########################################################################

    def __repr__(self):
        return repr(self.__forward)

    def __lt__(self, other):
        return self.__forward < other

    def __le__(self, other):
        return self.__forward <= other

    def __eq__(self, other):
        return self.__forward == other

    def __ne__(self, other):
        return self.__forward != other

    def __gt__(self, other):
        return self.__forward > other

    def __ge__(self, other):
        return self.__forward >= other

    def __len__(self):
        return len(self.__forward)

    def __getitem__(self, key):
        if key in self:
            return self.__forward[key]
        return self.__missing_key(key)

    def __setitem__(self, key, value):
        if self.in_values(value):
            del self[self.get_key(value)]
        self.__set_key_value(key, value)
        return value

    def __delitem__(self, key):
        self.pop(key)

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

    def __contains__(self, key):
        return key in self.__forward

    ########################################################################

    def clear(self):
        self.__forward = {}
        self.__reverse = {}

    def copy(self):
        return self.__class__(self.items())

    def del_value(self, value):
        self.pop_key(value)

    def get(self, key, default=None):
        return self[key] if key in self else default

    def get_key(self, value):
        if self.in_values(value):
            return self.__reverse[value]
        return self.__missing_value(value)

    def get_key_default(self, value, default=None):
        return self.get_key(value) if self.in_values(value) else default

    def in_values(self, value):
        return value in self.__reverse

    def items(self):
        return self.__dict_view('items', ((key, self[key]) for key in self))

    def iter_values(self):
        return iter(self.__reverse)

    def keys(self):
        return self.__dict_view('keys', self.__forward)

    def pop(self, key, *default):
        if len(default) > 1:
            raise TypeError('too many arguments')
        if key in self:
            value = self[key]
            self.__del_key_value(key, value)
            return value
        if default:
            return default[0]
        raise KeyError(key)

    def pop_key(self, value, *default):
        if len(default) > 1:
            raise TypeError('too many arguments')
        if self.in_values(value):
            key = self.get_key(value)
            self.__del_key_value(key, value)
            return key
        if default:
            return default[0]
        raise KeyError(value)

    def popitem(self):
        try:
            key = next(iter(self))
        except StopIteration:
            raise KeyError('popitem(): dictionary is empty')
        return key, self.pop(key)

    def set_key(self, value, key):
        if key in self:
            self.del_value(self[key])
        self.__set_key_value(key, value)
        return key

    def setdefault(self, key, default=None):
        if key not in self:
            self[key] = default
        return self[key]

    def setdefault_key(self, value, default=None):
        if not self.in_values(value):
            self.set_key(value, default)
        return self.get_key(value)

    def update(self, iterable=(), **pairs):
        for key, value in (((key, iterable[key]) for key in iterable.keys())
                           if hasattr(iterable, 'keys') else iterable):
            self[key] = value
        for key, value in pairs.items():
            self[key] = value

    def values(self):
        return self.__dict_view('values', self.__reverse)

    ########################################################################

    def __missing_key(self, key):
        if hasattr(self.__class__, '__missing__'):
            return self.__missing__(key)
        if not hasattr(self, 'default_factory') \
           or self.default_factory is None:
            raise KeyError(key)
        return self.__setitem__(key, self.default_factory())

    def __missing_value(self, value):
        if hasattr(self.__class__, '__missing_value__'):
            return self.__missing_value__(value)
        if not hasattr(self, 'default_key_factory') \
           or self.default_key_factory is None:
            raise KeyError(value)
        return self.set_key(value, self.default_key_factory())

    def __set_key_value(self, key, value):
        self.__forward[key] = value
        self.__reverse[value] = key

    def __del_key_value(self, key, value):
        del self.__forward[key]
        del self.__reverse[value]

    ########################################################################

    class __dict_view(frozenset):

        __slots__ = '__name'

        def __new__(cls, name, iterable=()):
            instance = super().__new__(cls, iterable)
            instance.__name = name
            return instance

        def __repr__(self):
            return 'dict_{}({})'.format(self.__name, list(self))
Noctis Skytower
  • 21,433
  • 16
  • 79
  • 117
6
# oneline solution using zip
>> x = {'a':100, 'b':999}
>> y = dict(zip(x.values(), x.keys()))  
>> y
{100: 'a', 999: 'b'}
NotTooTechy
  • 448
  • 5
  • 9
5

No, you can not do this efficiently without looking in all the keys and checking all their values. So you will need O(n) time to do this. If you need to do a lot of such lookups you will need to do this efficiently by constructing a reversed dictionary (can be done also in O(n)) and then making a search inside of this reversed dictionary (each search will take on average O(1)).

Here is an example of how to construct a reversed dictionary (which will be able to do one to many mapping) from a normal dictionary:

for i in h_normal:
    for j in h_normal[i]:
        if j not in h_reversed:
            h_reversed[j] = set([i])
        else:
            h_reversed[j].add(i)

For example if your

h_normal = {
  1: set([3]), 
  2: set([5, 7]), 
  3: set([]), 
  4: set([7]), 
  5: set([1, 4]), 
  6: set([1, 7]), 
  7: set([1]), 
  8: set([2, 5, 6])
}

your h_reversed will be

{
  1: set([5, 6, 7]),
  2: set([8]), 
  3: set([1]), 
  4: set([5]), 
  5: set([8, 2]), 
  6: set([8]), 
  7: set([2, 4, 6])
}
Salvador Dali
  • 214,103
  • 147
  • 703
  • 753
5

Make a reverse dictionary

reverse_dictionary = {v:k for k,v in dictionary.items()} 

If you have a lot of reverse lookups to do

imbr
  • 6,226
  • 4
  • 53
  • 65
2

There isn't one as far as I know of, one way however to do it is to create a dict for normal lookup by key and another dict for reverse lookup by value.

There's an example of such an implementation here:

http://code.activestate.com/recipes/415903-two-dict-classes-which-can-lookup-keys-by-value-an/

This does mean that looking up the keys for a value could result in multiple results which can be returned as a simple list.

Jonathan Holloway
  • 62,090
  • 32
  • 125
  • 150
1

I know this might be considered 'wasteful', but in this scenario I often store the key as an additional column in the value record:

d = {'key1' : ('key1', val, val...), 'key2' : ('key2', val, val...) }

it's a tradeoff and feels wrong, but it's simple and works and of course depends on values being tuples rather than simple values.

CarlS
  • 528
  • 5
  • 9
0

Through values in dictionary can be object of any kind they can't be hashed or indexed other way. So finding key by the value is unnatural for this collection type. Any query like that can be executed in O(n) time only. So if this is frequent task you should take a look for some indexing of key like Jon sujjested or maybe even some spatial index (DB or http://pypi.python.org/pypi/Rtree/ ).

Odomontois
  • 15,918
  • 2
  • 36
  • 71
-1

I'm using dictionaries as a sort of "database", so I need to find a key that I can reuse. For my case, if a key's value is None, then I can take it and reuse it without having to "allocate" another id. Just figured I'd share it.

db = {0:[], 1:[], ..., 5:None, 11:None, 19:[], ...}

keys_to_reallocate = [None]
allocate.extend(i for i in db.iterkeys() if db[i] is None)
free_id = keys_to_reallocate[-1]

I like this one because I don't have to try and catch any errors such as StopIteration or IndexError. If there's a key available, then free_id will contain one. If there isn't, then it will simply be None. Probably not pythonic, but I really didn't want to use a try here...

Zizouz212
  • 4,908
  • 5
  • 42
  • 66
-12

There is none. Don't forget that the value may be found on any number of keys, including 0 or more than 1.

Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
  • 2
    python has a .index method on lists the returns the first found index with the specified value or an exception if not found... any reason why such a semantic could not be applied to dictionaries? – Brian Jack Jun 22 '12 at 15:27
  • @BrianJack: Dictionaries are not ordered, like sets. Look at collections.OrderedDict for an implementation that *is* ordered. – Martijn Pieters Jul 24 '12 at 14:40
  • 3
    .index only needs to guarantee that it returns a single value and it does not need to be lexically first only that it be the first match and that it's behavior is stable (multiple calls on same dict over time should yield same matching element). Unless dictionaries rearrange their unmodified hashes over time as other elements get added, removed or modified it would still work suitably. A naive implementation: dictObject.items().index(key) – Brian Jack Jul 25 '12 at 19:43
  • the point mainly of .index() is that by definition we don't care about duplicates only that we may look up a single element consistently – Brian Jack Jul 25 '12 at 19:51
  • 161
    **I abhor non-answers like this.** "Stop trying to do what you justifiably want to do!" is _not_ an acceptable answer. Why was this accepted? As higher-rated answers to this question attest, reverse dictionary lookup is trivially implementable in less than 80 characters of pure-Python. It doesn't get any more "straight forward" than that. [Paul McGuire](https://stackoverflow.com/users/165216/paul-mcguire)'s [solution](https://stackoverflow.com/a/2569076/2809027) is probably the most efficient, but they _all_ work. `` – Cecil Curry Jun 28 '16 at 02:04
  • 1
    Including the above response from @CecilCurry in Python3 (3.x?) Dictionaries *are* now ordered, as with sets. (though sets will be a random order every time you create one, a single instance will not rearrange. Not sure about Dictionaries but imagine it's similar.) – ch4rl1e97 Mar 26 '20 at 15:21