How to find the index of an item in a Python list

list.index() answers the question in one call. What it does when the item is not there, when the item appears twice, and what it costs on a large list are the parts worth knowing.

The answer

colours = ["red", "green", "blue", "green"]
colours.index("green")
colours.index('green') : 1

Note that green appears at index 1 and index 3. list.index() returns the first match and stops looking.

When the item is not there

colours.index('purple') -> ValueError: list.index(x): x not in list

It raises. It does not return -1, which is what str.find() does for strings — a genuine inconsistency in the standard library and a reliable source of surprise.

The obvious guard is to check first:

if "purple" in colours:
    i = colours.index("purple")

That works, and it walks the list twice: once for the in, once for the index. Measured over 2000 lookups of the last element in a 10,000-item list:

'in' then .index()      : 0.2877 s for 2000 runs
try/except around index : 0.1600 s for 2000 runs
ratio                   : 1.80x

So the idiomatic Python version is also the faster one:

try:
    i = colours.index("purple")
except ValueError:
    i = -1

A default, without the try/except

For a one-liner, a generator with next() and a default:

next((i for i, c in enumerate(colours) if c == "purple"), None)
next(generator, None) : None

This form has a second advantage: it matches on a condition, which .index() cannot do at all. Finding the first record satisfying something is the case you actually hit:

idx = next((i for i, p in enumerate(people) if p["age"] > 40), None)
first person over 40 is at index 1 -> alan

Every occurrence

[i for i, c in enumerate(colours) if c == "green"]
[1, 3]

enumerate is the right tool the moment you need more than the first hit.

If you only want the next one after a known position, index() takes start and stop arguments, the same as a slice:

first  : 1
second : 3

It compares with ==, not identity

Worth knowing because it decides what “found” means. Here is a class that claims to equal everything:

weird.index('anything') : 0

More practically, this is why float('nan') behaves oddly:

float('nan') in [nan]              : True
[float('nan')].index(float('nan')) -> ValueError

in on the same nan object succeeds because the containment check tries identity before equality. Two different nan objects are neither identical nor equal — nan != nan by IEEE 754 — so .index() finds nothing.

When to stop using it

.index() scans from the front, so it is O(n). For one lookup that is fine. For repeated lookups, build a dict once:

table = {value: i for i, value in enumerate(words)}
list.index() on 50,000 items : 0.0879 s for 200 lookups
dict lookup                  : 0.000005 s for 200 lookups
ratio                        : 17227x

Four orders of magnitude. Building the dict costs one pass, and every lookup after that is effectively free.

If the list is sorted, there is a middle option — bisect, which is O(log n) and needs no extra memory:

.index() : 0.1681 s   bisect : 0.000026 s   ratio 6384x

The caveat is that bisect assumes the list is sorted and does not check. On unsorted input it returns a confident wrong answer.

In a nested list

There is no built-in for this, but the comprehension is short enough:

next(((r, c) for r, row in enumerate(grid) for c, v in enumerate(row) if v == "d"), None)
position of 'd' in [['a', 'b'], ['c', 'd']] : (1, 1)

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).