168

I have code which looks something like this:

thing_index = thing_list.index(thing)
otherfunction(thing_list, thing_index)

ok so that's simplified but you get the idea. Now thing might not actually be in the list, in which case I want to pass -1 as thing_index. In other languages this is what you'd expect index() to return if it couldn't find the element. In fact it throws a ValueError.

I could do this:

try:
    thing_index = thing_list.index(thing)
except ValueError:
    thing_index = -1
otherfunction(thing_list, thing_index)

But this feels dirty, plus I don't know if ValueError could be raised for some other reason. I came up with the following solution based on generator functions, but it seems a little complex:

thing_index = ( [(i for i in xrange(len(thing_list)) if thing_list[i]==thing)] or [-1] )[0]

Is there a cleaner way to achieve the same thing? Let's assume the list isn't sorted.

Tim Pietzcker
  • 328,213
  • 58
  • 503
  • 561
Draemon
  • 33,955
  • 16
  • 77
  • 104
  • 6
    "...in which case I want to pass -1 as `thing_index`." - This is definitely un-Pythonic. Passing a (meaningless) token value in case an operation does not succeed is frowned upon - exceptions really are the right way here. Especially since `thing_list[-1]` is a valid expression, meaning the last entry in the list. – Tim Pietzcker Jan 25 '10 at 14:26
  • 1
    @jellybean: *facepalm*...spot the java coder :P – Draemon Jan 25 '10 at 14:26
  • 5
    @Tim: there is `str.find` method that does exactly that: returns `-1` when needle is not found in subject. – SilentGhost Jan 25 '10 at 14:27
  • @Tim None would be better then...and this would be analogous to dict[key] vs dict.get[key] – Draemon Jan 25 '10 at 14:28
  • @SilentGhost: Hm, interesting. I might have to look into this in more detail. `str.index()` does throw an exception if the search string is not found. – Tim Pietzcker Jan 25 '10 at 14:29
  • 1
    The lack of such useful functionality on the builtin list type appears to be a poor design decision. When an exception is raised, it suggests that something bad/unexpected happened. Yet the question "Is this value in the list, and if so, what is its index?" forms a typical fork within the happy path of many programs. This design flaw forces programmers to pick the lesser of two evils: 1) sacrifice performance and iterate through the list twice to get the index of the value, or 2) sacrifice readability and pretend that it is an 'exception' if the value isn't in the list. – John Colvin Apr 06 '21 at 00:26
  • One day we will assume: python has some freak features. – Daniel Bandeira Jul 25 '21 at 12:25

16 Answers16

84
thing_index = thing_list.index(elem) if elem in thing_list else -1

One line. Simple. No exceptions.

Emil Ivanov
  • 37,300
  • 12
  • 75
  • 90
  • 65
    Simple yes, but that will do two linear searches and while performance isn't an issue per-se, that seems excessive. – Draemon Jan 25 '10 at 15:16
  • 6
    @Draemon: Agree - that will do 2 passes - but it's unlikely that from a thousand-line code-base this one will be the bottleneck. :) One can always opt-in for an imperative solution with `for`. – Emil Ivanov Jan 25 '10 at 15:40
  • 1
    with lambd `indexOf = lambda item,list_ : list_.index(item) if item in list_ else -1 # OR None ` – Alaa Aqeel Jun 15 '19 at 15:39
82

There is nothing "dirty" about using try-except clause. This is the pythonic way. ValueError will be raised by the .index method only, because it's the only code you have there!

To answer the comment:
In Python, easier to ask forgiveness than to get permission philosophy is well established, and no index will not raise this type of error for any other issues. Not that I can think of any.

SilentGhost
  • 307,395
  • 66
  • 306
  • 293
  • 40
    Surely exceptions are for exceptional cases, and this is hardly that. I wouldn't have such a problem if the exception was more specific than ValueError. – Draemon Jan 25 '10 at 14:06
  • 1
    I know it can only be thrown from that *method* but is it guaranteed to only be thrown for that *reason*? Not that I can think of another reason index would fail..but then aren't exceptions for exactly those things you may not think of? – Draemon Jan 25 '10 at 14:08
  • @Draemon: That's why it checks only for `ValueError` and not just any form of exception. – MAK Jan 25 '10 at 14:18
  • 5
    Isn't `{}.get(index, '')` more pythonic? Not to mention shorter more readable. – Esteban Küber Jan 25 '10 at 14:19
  • @voyager: `.get` is not defined for lists. It's not clear what dictionary has to do with this question. – SilentGhost Jan 25 '10 at 14:26
  • 2
    I use dict[key] when I expect the key to exist and dict.get(key) when I'm not sure, and I *am* looking for something equivalent here. Returning `None` instead of -1 would be fine, but as you commented yourself, str.find() returns -1 so why shouldn't there be list.find() that does the same thing? I'm not buying the "pythonic" argument – Draemon Jan 25 '10 at 14:32
  • @Draemon: well `str.find` is a rudiment. There isn't a built-in equivalent, the code that you have is 100% equivalent to what `find` would look like if it'd been written in Python. – SilentGhost Jan 25 '10 at 14:37
  • 4
    But the point is that the most pythonic solution is to use *only* try/except and not the -1 sentinel value at all. I.E. you should rewrite `otherfunction`. On the other hand, if it ain't broke, ... – Andrew Jaffe Jan 25 '10 at 15:49
  • 1
    I don't understand why `ValueError` would only be raised by the `index()` method only. I mean what if you had `somelist.index(somefun(somedata))` you don't know if `somefun` won't raise `ValueError` somewhere. All this `EFTP` stuff doesn't really make sense in a lot of cases – FacelessPanda Oct 08 '17 at 09:31
22

The dict type has a get function, where if the key doesn't exist in the dictionary, the 2nd argument to get is the value that it should return. Similarly there is setdefault, which returns the value in the dict if the key exists, otherwise it sets the value according to your default parameter and then returns your default parameter.

You could extend the list type to have a getindexdefault method.

class SuperDuperList(list):
    def getindexdefault(self, elem, default):
        try:
            thing_index = self.index(elem)
            return thing_index
        except ValueError:
            return default

Which could then be used like:

mylist = SuperDuperList([0,1,2])
index = mylist.getindexdefault( 'asdf', -1 )
Ross Rogers
  • 23,523
  • 27
  • 108
  • 164
19

If you are doing this often then it is better to stow it away in a helper function:

def index_of(val, in_list):
    try:
        return in_list.index(val)
    except ValueError:
        return -1 
Veneet Reddy
  • 2,707
  • 1
  • 24
  • 40
  • 7
    Don’t forget `-1` is a valid index: `in_list[-1]`, though obviously not returned from `index()`. Possibly return `None`? – Manngo Jul 26 '21 at 00:48
12

There is nothing wrong with your code that uses ValueError. Here's yet another one-liner if you'd like to avoid exceptions:

thing_index = next((i for i, x in enumerate(thing_list) if x == thing), -1)
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • Is that python 2.6? I know I didn't mention it, but I'm using 2.5. This is probably what I'd do in 2.6 – Draemon Jan 25 '10 at 15:12
  • 1
    @Draemon: Yes, `next()` function exists in Python 2.6+ . But it is easy to implement for 2.5, see [next() function implementation for Python 2.5](http://stackoverflow.com/questions/500578/is-there-an-alternative-way-of-calling-next-on-python-generators/500609#500609) – jfs Jan 25 '10 at 16:00
9

What about this:

li = [1,2,3,4,5] # create list 

li = dict(zip(li,range(len(li)))) # convert List To Dict 
print( li ) # {1: 0, 2: 1, 3: 2, 4: 3, 5: 4}
li.get(20) # None 
li.get(1)  # 0 
Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
Alaa Aqeel
  • 615
  • 8
  • 16
  • 1
    This is exactly what I wanted: To get a default value if the element is not found - writing a function on or a if-else for this seems like an overkill. – tpb261 Oct 28 '20 at 14:01
6

This issue is one of language philosophy. In Java for example there has always been a tradition that exceptions should really only be used in "exceptional circumstances" that is when errors have happened, rather than for flow control. In the beginning this was for performance reasons as Java exceptions were slow but now this has become the accepted style.

In contrast Python has always used exceptions to indicate normal program flow, like raising a ValueError as we are discussing here. There is nothing "dirty" about this in Python style and there are many more where that came from. An even more common example is StopIteration exception which is raised by an iterator‘s next() method to signal that there are no further values.

Tendayi Mawushe
  • 25,562
  • 6
  • 51
  • 57
  • Actually, the JDK throws *way* too many checked exceptions, so I'm not sure that philosophy is actually applied to Java. I don't have a problem per-se with `StopIteration` because it's clearly-defined what the exception means. `ValueError` is just a little too generic. – Draemon Jan 25 '10 at 15:15
  • I was referring to the idea that exceptions should not be used for flow control: http://c2.com/cgi/wiki?DontUseExceptionsForFlowControl, not so much the number of checked exceptions that Java has which a whole other discussion: http://www.mindview.net/Etc/Discussions/CheckedExceptions – Tendayi Mawushe Jan 25 '10 at 15:24
2

What about this:

otherfunction(thing_collection, thing)

Rather than expose something so implementation-dependent like a list index in a function interface, pass the collection and the thing and let otherfunction deal with the "test for membership" issues. If otherfunction is written to be collection-type-agnostic, then it would probably start with:

if thing in thing_collection:
    ... proceed with operation on thing

which will work if thing_collection is a list, tuple, set, or dict.

This is possibly clearer than:

if thing_index != MAGIC_VALUE_INDICATING_NOT_A_MEMBER:

which is the code you already have in otherfunction.

PaulMcG
  • 62,419
  • 16
  • 94
  • 130
1

I'd suggest:

if thing in thing_list:
  list_index = -1
else:
  list_index = thing_list.index(thing)
Jonas
  • 31
  • 1
  • 2
    Problem with this solution is that "-1" is a valid index in to list (last index; the first from the end). Better way to handle this would be return False in first branch of your condition. – FanaticD Apr 11 '15 at 18:15
  • Note that your statements are inverted; the `list_index = -1` should be in the `else` part and vice versa. Otherwise, I think that the `thing in thing_list` is the best way to avoid the `try`/`catch`. – Alexis Wilke May 18 '22 at 03:41
1

What about like this:

temp_inx = (L + [x]).index(x) 
inx = temp_inx if temp_inx < len(L) else -1
Petter Friberg
  • 21,252
  • 9
  • 60
  • 109
Jie Xiong
  • 11
  • 1
1

It's been quite some time but it's a core part of the stdlib and has dozens of potential methods so I think it's useful to have some benchmarks for the different suggestions and include the numpy method which can be by far the fastest.

import random
from timeit import timeit
import numpy as np

l = [random.random() for i in range(10**4)]
l[10**4 - 100] = 5

# method 1
def fun1(l:list, x:int, e = -1) -> int:
    return [[i for i,elem in enumerate(l) if elem == x] or [e]][0]

# method 2
def fun2(l:list, x:int, e = -1) -> int:
    for i,elem in enumerate(l):
        if elem == x:
            return i
    else:
        return e

# method 3
def fun3(l:list, x:int, e = -1) -> int:
    try:
        idx = l.index(x)
    except ValueError:
        idx = e
    return idx

# method 4
def fun4(l:list, x:int, e = -1) -> int:
    return l.index(x) if x in l else e

l2 = np.array(l)
# method 5
def fun5(l:list or np.ndarray, x:int, e = -1) -> int:
    res = np.where(np.equal(l, x))
    if res[0].any():
        return res[0][0]
    else:        
        return e


if __name__ == "__main__":
    print("Method 1:")
    print(timeit(stmt = "fun1(l, 5)", number = 1000, globals = globals()))
    print("")
    print("Method 2:")
    print(timeit(stmt = "fun2(l, 5)", number = 1000, globals = globals()))
    print("")
    print("Method 3:")
    print(timeit(stmt = "fun3(l, 5)", number = 1000, globals = globals()))
    print("")
    print("Method 4:")
    print(timeit(stmt = "fun4(l, 5)", number = 1000, globals = globals()))
    print("")
    print("Method 5, numpy given list:")
    print(timeit(stmt = "fun5(l, 5)", number = 1000, globals = globals()))
    print("")
    print("Method 6, numpy given np.ndarray:")
    print(timeit(stmt = "fun5(l2, 5)", number = 1000, globals = globals()))
    print("")

When run as main, this results in the following printout on my machine indicating time in seconds to complete 1000 trials of each function:

Method 1: 0.7502102799990098

Method 2: 0.7291318440002215

Method 3: 0.24142152300009911

Method 4: 0.5253471979995084

Method 5, numpy given list: 0.5045417560013448

Method 6, numpy given np.ndarray: 0.011147511999297421

Of course the question asks specifically about lists so the best solution is to use the try-except method, however the speed improvements (at least 20x here compared to try-except) offered by using the numpy data structures and operators instead of python data structures is significant and if building something on many arrays of data that is performance critical then the author should try to use numpy throughout to take advantage of the superfast C bindings. (CPython interpreter, other interpreter performances may vary)

Btw, the reason Method 5 is much slower than Method 6 is because numpy first has to convert the given list to it's own numpy array, so giving it a list doesn't break it it just doesn't fully utilise the speed possible.

G.S
  • 535
  • 3
  • 8
1

I'll be blunt: the answers here are very bad, and have insane time complexity.

Here's a simple way.

With dict().get('key', 'some_value'), the value at 'key' will be returned, and if the key is not in the dictionary, 'some_value' will be returned.

You can create such a dictionary with your list and its indices.

mylist = ['cat' 'dog', 'bunny']

mapping = {value: index for index, value in enumerate(mylist)}

Then, mapping.get('key', 0) will return the index if found, or None.

mapping.get('penguin', 0)  # returns 0
Nicolas Gervais
  • 33,817
  • 13
  • 115
  • 143
0

I have the same issue with the ".index()" method on lists. I have no issue with the fact that it throws an exception but I strongly disagree with the fact that it's a non-descriptive ValueError. I could understand if it would've been an IndexError, though.

I can see why returning "-1" would be an issue too because it's a valid index in Python. But realistically, I never expect a ".index()" method to return a negative number.

Here goes a one liner (ok, it's a rather long line ...), goes through the list exactly once and returns "None" if the item isn't found. It would be trivial to rewrite it to return -1, should you so desire.

indexOf = lambda list, thing: \
            reduce(lambda acc, (idx, elem): \
                   idx if (acc is None) and elem == thing else acc, list, None)

How to use:

>>> indexOf([1,2,3], 4)
>>>
>>> indexOf([1,2,3], 1)
0
>>>
emvee
  • 4,371
  • 23
  • 23
0

Comparison of implementations

Simple comparison on Python 3.8

TL;DR maybeidx2 is faster in general except for arrays (n<100) with lots of misses

def maybeidx1(l, v):
    return l.index(v) if v in l else None

def maybeidx2(l, v):
    try:
        return l.index(v)
    except ValueError:
        return None

Test cases:

a = [*range(100_000)]
# Case 1: index in list
maybeidx1(a, 50_000)
Out[20]: 50000
maybeidx2(a, 50_000)
Out[21]: 50000
# Case 2: index not in list
maybeidx1(a, 100_000) is None
Out[23]: True
maybeidx2(a, 100_000) is None
Out[24]: True

Timing case 1

%timeit maybeidx1(a, 50_000)
1.06 ms ± 15.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit maybeidx2(a, 50_000)
530 µs ± 8.47 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Timing case 2

%timeit maybeidx1(a, 100_000)
1.07 ms ± 21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit maybeidx2(a, 100_000)
1.07 ms ± 16.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Result

Use the maybeidx2 method for larger arrays. This is faster due to maybeidx1 having two scans of the array in search of the value - this is still O(n) time but has a constant multiplier of 2 and thus slower in practice. This holds in the case of the value being present in the list. When the value is not present, these times will be roughly equal; they both have to scan the full array exactly once, then return None. The overhead of the try-except is negligible, even with an array size of 10 - unless case two occurs. Then the try-except overhead is noticeable. Example:

a = [*range(10)]
%timeit maybeidx1(a, 10)
191 ns ± 2.61 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit maybeidx2(a, 10)
566 ns ± 5.93 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

This overhead becomes negligible (on my machine) when a has above 100 elements.

craymichael
  • 4,578
  • 1
  • 15
  • 24
0

Since Python 3.6, there are find and rfind methods for index and rindex which return -1 instead of throwing exception.

Kyaw Tun
  • 12,447
  • 10
  • 56
  • 83
-1

I don't know why you should think it is dirty... because of the exception? if you want a oneliner, here it is:

thing_index = thing_list.index(elem) if thing_list.count(elem) else -1

but i would advise against using it; I think Ross Rogers solution is the best, use an object to encapsulate your desiderd behaviour, don't try pushing the language to its limits at the cost of readability.

Alan Franzoni
  • 3,041
  • 1
  • 23
  • 35
  • 1
    Yes, because of the exception. Your code will do two linear searches won't it? Not that performance really matters here. The SuperDuperList solution is nice, but seems like overkill in this particular situation. I think I'll end up just catching the exception, but I wanted to see if there was a cleaner (to my aesthetic) way. – Draemon Jan 25 '10 at 14:36
  • @Draemon: well you'll encapsulate the code you have into the `find()` function and it will be all clean ;) – SilentGhost Jan 25 '10 at 14:38
  • 1
    It's curious that my answer has two downvotes, while Emil Ivanov's, while semantically identical, is the one of the most upvoted. Most probably this happens because mine is slower, since I employed count() instead of the "in" operator... at least a comment saying that would have been great, though :-) – Alan Franzoni Sep 07 '16 at 15:10