168

I have a list of dicts:

list = [{'id':'1234','name':'Jason'},
        {'id':'2345','name':'Tom'},
        {'id':'3456','name':'Art'}]

How can I efficiently find the index position [0],[1], or [2] by matching on name = 'Tom'?

If this were a one-dimensional list I could do list.index() but I'm not sure how to proceed by searching the values of the dicts within the list.

Serenity
  • 35,289
  • 20
  • 120
  • 115
ensnare
  • 40,069
  • 64
  • 158
  • 224
  • 7
    "list" is the list constructor, you better choose another name for a list (even in a example). And what should be the response if no element is found? raise an exception? return None? – tokland Dec 08 '10 at 20:04
  • 8
    If you're going to need this a lot, use a more appropriate data structure (perhaps `{ 'Jason': {'id': '1234'}, 'Tom': {'id': '1245'}, ...}`?) –  Dec 08 '10 at 20:06
  • 3
    @delnan Because that's a recipe for disaster! If anything, it should be `{'1234': {'name': 'Jason'}, ...}`. Not that that would help this use-case. – OJFord Sep 09 '16 at 13:38

12 Answers12

205
lst = [{'id':'1234','name':'Jason'}, {'id':'2345','name':'Tom'}, {'id':'3456','name':'Art'}]

tom_index = next((index for (index, d) in enumerate(lst) if d["name"] == "Tom"), None)
# 1

If you need to fetch repeatedly from name, you should index them by name (using a dictionary), this way get operations would be O(1) time. An idea:

def build_dict(seq, key):
    return dict((d[key], dict(d, index=index)) for (index, d) in enumerate(seq))

people_by_name = build_dict(lst, key="name")
tom_info = people_by_name.get("Tom")
# {'index': 1, 'id': '2345', 'name': 'Tom'}
tokland
  • 66,169
  • 13
  • 144
  • 170
  • 3
    IMHO this is not as readable or Pythonic is @Emile's answer. Because the intention is not really to create a generator (and using `next()` for this seems weird to me), the aim is just to get the index. Also, this raises StopIteration, whereas the Python `lst.index()` method raises ValueError. – Ben Hoyt Dec 08 '10 at 20:36
  • @benhoyt: I don't like the StopIteration exception either, but while you can change next()'s default value, the exception it raises is fixed. The pythonicity is somewhat subjective so I won't dispute it, probably a for-loop is more pythonic. On the other hand, some people aliases next() for first(), and that definitely sounds better: first(index for (index, d) in ...). – tokland Dec 08 '10 at 21:03
  • `first()` does sound better. You could always try/except the StopIteration and raise ValueError so the caller has consistency. Alternatively set `next()`'s default to -1. – Ben Hoyt Dec 08 '10 at 21:10
  • This is one of the things that python got wrong. Not allowing a optional comparator for the index function. Code like this is a nightmare to maintain. – boatcoder May 01 '13 at 19:17
  • `next(index for (index, d) in enumerate(lst) if d["name"] == "Tom", None)` will return `None` instead of raising an exception if nothing is found. – gdw2 May 08 '15 at 04:20
  • @gdw2: yes, we discussed it with benhoyt on earlier comments. – tokland May 08 '15 at 09:45
  • 1
    @gdw2: I get `SyntaxError: Generator expression must be parenthesized if not sole argument` when doing that. – avoliva Jun 16 '16 at 18:12
  • 2
    @avoliva add a parenthesis around next like follows `next((index for (index, d) in enumerate(lst) if d["name"] == "Tom"), None)` – HussienK Dec 06 '16 at 15:54
  • I need first to know what is lst? – lfvv Oct 21 '20 at 03:12
70

A simple readable version is

def find(lst, key, value):
    for i, dic in enumerate(lst):
        if dic[key] == value:
            return i
    return -1
Emile
  • 2,946
  • 2
  • 19
  • 22
  • 9
    This seems the most readable and Pythonic. It also mimics the behaviour of `str.find()` nicely. You could also call it `index()` and raise a `ValueError` instead of returning -1 if that was preferable. – Ben Hoyt Dec 08 '10 at 20:38
  • 14
    Agreed - by returning -1 when no match is found, you'll always get the last dict in the list, which is probably not what you want. Better to return None and check for exsistence of a match in the calling code. – shacker Jun 13 '16 at 17:54
13

It won't be efficient, as you need to walk the list checking every item in it (O(n)). If you want efficiency, you can use dict of dicts. On the question, here's one possible way to find it (though, if you want to stick to this data structure, it's actually more efficient to use a generator as Brent Newey has written in the comments; see also tokland's answer):

>>> L = [{'id':'1234','name':'Jason'},
...         {'id':'2345','name':'Tom'},
...         {'id':'3456','name':'Art'}]
>>> [i for i,_ in enumerate(L) if _['name'] == 'Tom'][0]
1
aeter
  • 11,960
  • 6
  • 27
  • 29
  • 1
    You can gain the efficiency you desire by using a generator. See tokland's answer. – Brent Newey Dec 08 '10 at 20:07
  • 2
    @Brent Newey: The generator does not change the fact, that you have to traverse the entire list, making the search O(n) as aeter claims... Depending on how long that list is, the difference between using a generator vs using a for loop or whatever might be neglible, wheras the difference between using a dict vs. using a list might not – Dirk Dec 08 '10 at 20:11
  • @Brent: You are right, but can it beat a O(1) lookup in a dictionary, moreover if the searched item is at the end of the list? – aeter Dec 08 '10 at 20:11
  • 1
    @Dirk The next() call on the generator stops when a match is found, therefore it does not have to traverse the entire list. – Brent Newey Dec 08 '10 at 20:14
  • @aeter You make a fair point. I was referring to being able to stop when a match is found. – Brent Newey Dec 08 '10 at 20:16
  • @Brent Newey: Assuming that you have to search half of the list on average to find the item you are looking for (some elements may be at the front of the list, others at the back) this gives you an expected run-time complexity of O(1/2n) = O(n) with a particular worst-case of always having to traverse the entire list, if the element is not present. Depends on your data, whether this is a frequent case. Stopping early does not change that. – Dirk Dec 09 '10 at 12:10
  • Dict of dicts solved a similar issue for me in a very nice way +1 :) – Simon Nicholls Feb 17 '16 at 17:54
4

Seems most logical to use a filter/index combo:

names=[{}, {'name': 'Tom'},{'name': 'Tony'}]
names.index(next(filter(lambda n: n.get('name') == 'Tom', names)))
1

And if you think there could be multiple matches:

[names.index(item) for item in filter(lambda n: n.get('name') == 'Tom', names)]
[1]
Andre Arruda
  • 59
  • 1
  • 5
michael salmon
  • 398
  • 4
  • 12
3

Answer offered by @faham is a nice one-liner, but it doesn't return the index to the dictionary containing the value. Instead it returns the dictionary itself. Here is a simple way to get: A list of indexes one or more if there are more than one, or an empty list if there are none:

list = [{'id':'1234','name':'Jason'},
        {'id':'2345','name':'Tom'},
        {'id':'3456','name':'Art'}]

[i for i, d in enumerate(list) if 'Tom' in d.values()]

Output:

>>> [1]

What I like about this approach is that with a simple edit you can get a list of both the indexes and the dictionaries as tuples. This is the problem I needed to solve and found these answers. In the following, I added a duplicate value in a different dictionary to show how it works:

list = [{'id':'1234','name':'Jason'},
        {'id':'2345','name':'Tom'},
        {'id':'3456','name':'Art'},
        {'id':'4567','name':'Tom'}]

[(i, d) for i, d in enumerate(list) if 'Tom' in d.values()]

Output:

>>> [(1, {'id': '2345', 'name': 'Tom'}), (3, {'id': '4567', 'name': 'Tom'})]

This solution finds all dictionaries containing 'Tom' in any of their values.

stanely
  • 342
  • 1
  • 8
2

Here's a function that finds the dictionary's index position if it exists.

dicts = [{'id':'1234','name':'Jason'},
         {'id':'2345','name':'Tom'},
         {'id':'3456','name':'Art'}]

def find_index(dicts, key, value):
    class Null: pass
    for i, d in enumerate(dicts):
        if d.get(key, Null) == value:
            return i
    else:
        raise ValueError('no dict with the key and value combination found')

print find_index(dicts, 'name', 'Tom')
# 1
find_index(dicts, 'name', 'Ensnare')
# ValueError: no dict with the key and value combination found
martineau
  • 119,623
  • 25
  • 170
  • 301
1

One liner!?

elm = ([i for i in mylist if i['name'] == 'Tom'] or [None])[0]
faham
  • 842
  • 11
  • 17
1

I needed a more general solution to account for the possibility of multiple dictionaries in the list having the key value, and a straightforward implementation using list comprehension:

dict_indices = [i for i, d in enumerate(dict_list) if d[dict_key] == key_value] 
0

For a given iterable, more_itertools.locate yields positions of items that satisfy a predicate.

import more_itertools as mit


iterable = [
    {"id": "1234", "name": "Jason"},
    {"id": "2345", "name": "Tom"},
    {"id": "3456", "name": "Art"}
]

list(mit.locate(iterable, pred=lambda d: d["name"] == "Tom"))
# [1]

more_itertools is a third-party library that implements itertools recipes among other useful tools.

pylang
  • 40,867
  • 14
  • 129
  • 121
0
def search(itemID,list):
     return[i for i in list if i.itemID==itemID]
zpvk
  • 124
  • 1
  • 10
0

The following will return the index for the first matching item:

['Tom' == i['name'] for i in list].index(True)
Dan Boschen
  • 265
  • 1
  • 11
0

my answer is better in one a dictionary to use

food_time_dict = {"Lina": 312400, "Tom": 360054, "Den": 245800}
print(list(food_time_dict.keys()).index("Lina"))

I request keys from the dictionary, then I translate the list if it is not added, there will be an error then I use it as a list. but on your code:

lists = [{'id': '1234', 'name': 'Jason'},
         {'id': '2345', 'name': 'Tom'},
         {'id': '3456', 'name': 'Art'}]
    
    
def dict_in_lists_index(lists, search):  # function for convenience
    j = 0  # [j][i]
    for i in lists:
        try:  # try our varible search if not found in list
            return f"[{j}][{list(i.values()).index(search)}]"
            # small decor
        except ValueError: # error was ValueError
            pass # aa... what must was what you want to do
        j += 1 # not found? ok j++
    return "Not Found"
    
    
def dict_cropped_index(lists, search):
    for i in lists:
        try:
            return list(i.values()).index(search)
        except ValueError:
            pass
    return "Not Found"
    
    
print(dict_in_lists_index(lists, 'Tom')) # and end
print(dict_cropped_index(lists, 'Tom')) # now for sure end
Flair
  • 2,609
  • 1
  • 29
  • 41