122

Say I have this list:

li = ["a", "b", "a", "c", "x", "d", "a", "6"]

As far as help showed me, there is not a builtin function that returns the last occurrence of a string (like the reverse of index). So basically, how can I find the last occurrence of "a" in the given list?

0

17 Answers 17

135

If you are actually using just single letters like shown in your example, then str.rindex would work handily. This raises a ValueError if there is no such item, the same error class as list.index would raise. Demo:

>>> li = ["a", "b", "a", "c", "x", "d", "a", "6"]
>>> ''.join(li).rindex('a')
6

For the more general case you could use list.index on the reversed list:

>>> len(li) - 1 - li[::-1].index('a')
6

The slicing here creates a copy of the entire list. That's fine for short lists, but for the case where li is very large, efficiency can be better with a lazy approach:

def list_rindex(li, x):
    for i in reversed(range(len(li))):
        if li[i] == x:
            return i
    raise ValueError("{} is not in list".format(x))

One-liner version:

next(i for i in reversed(range(len(li))) if li[i] == 'a')
2
  • 3
    @Chris_Rands: Better than range(len(li)-1, -1, -1) is either reversed(range(len(li))) or range(len(li))[::-1] (they're roughly equivalent too, unlike most comparisons between reversed and the reversing slice; modern Py3 range can be sliced in reverse to make another lazy range that runs backwards, so it's equally performant). Looks like wim went with the former. Commented Sep 26, 2019 at 16:58
  • 2
    Pythonic version next(len(a)-i-1 for i, x in enumerate(reversed(a)) if x == 3) Commented Nov 2, 2020 at 11:35
60

A one-liner that's like Ignacio's except a little simpler/clearer would be

max(loc for loc, val in enumerate(li) if val == 'a')

It seems very clear and Pythonic to me: you're looking for the highest index that contains a matching value. No nexts, lambdas, reverseds or itertools required.

3
  • 8
    As @Isaac points out this always iterates over all N elements of li.
    – smci
    Commented Dec 14, 2014 at 12:44
  • 1
    a significant downside is the fact you will always iterate through the whole list. you should emphasis that in your answer.
    – Guy
    Commented Oct 10, 2018 at 8:47
  • 3
    Use default=None to avoid error "ValueError: max() arg is an empty sequence" max((loc for loc, val in enumerate(li) if val == 'a'), default=None) Commented Feb 1, 2022 at 11:08
21

Many of the other solutions require iterating over the entire list. This does not.

def find_last(lst, elm):
  gen = (len(lst) - 1 - i for i, v in enumerate(reversed(lst)) if v == elm)
  return next(gen, None)

Edit: In hindsight this seems like unnecessary wizardry. I'd do something like this instead:

def find_last(lst, sought_elt):
    for r_idx, elt in enumerate(reversed(lst)):
        if elt == sought_elt:
            return len(lst) - 1 - r_idx
2
  • @Claudio why do you think that his solution iterates over the entire list? Commented Apr 23 at 18:46
  • @ChristophRackwitz : thanks for the hint ... sorry ... have overseen the return next ...
    – oOosys
    Commented Apr 23 at 20:46
10
>>> (x for x in reversed(list(enumerate(li))) if x[1] == 'a').next()[0]
6

>>> len(li) - (x for x in enumerate(li[::-1]) if x[1] == 'a').next()[0] - 1
6
0
7

I like both wim's and Ignacio's answers. However, I think itertools provides a slightly more readable alternative, lambda notwithstanding. (For Python 3; for Python 2, use xrange instead of range).

>>> from itertools import dropwhile
>>> l = list('apples')
>>> l.index('p')
1
>>> next(dropwhile(lambda x: l[x] != 'p', reversed(range(len(l)))))
2

This will raise a StopIteration exception if the item isn't found; you could catch that and raise a ValueError instead, to make this behave just like index.

Defined as a function, avoiding the lambda shortcut:

def rindex(lst, item):
    def index_ne(x):
        return lst[x] != item
    try:
        return next(dropwhile(index_ne, reversed(range(len(lst)))))
    except StopIteration:
        raise ValueError("rindex(lst, item): item not in list")

It works for non-chars too. Tested:

>>> rindex(['apples', 'oranges', 'bananas', 'apples'], 'apples')
3
0
6
last_occurence=len(yourlist)-yourlist[::-1].index(element)-1

just easy as that.no need to import or create a function.

6

With dict

You can use the fact that dictionary keys are unique and when building one with tuples only the last assignment of a value for a particular key will be used. As stated in other answers, this is fine for small lists but it creates a dictionary for all unique values and might not be efficient for large lists.

dict(map(reversed, enumerate(li)))["a"]

6
2

I came here hoping to find someone had already done the work of writing the most efficient version of list.rindex, which provided the full interface of list.index (including optional start and stop parameters). I didn't find that in the answers to this question, or here, or here, or here. So I put this together myself... making use of suggestions from other answers to this and the other questions.

def rindex(seq, value, start=None, stop=None):
  """L.rindex(value, [start, [stop]]) -> integer -- return last index of value.
  Raises ValueError if the value is not present."""
  start, stop, _ = slice(start, stop).indices(len(seq))
  if stop == 0:
    # start = 0
    raise ValueError('{!r} is not in list'.format(value))
  else:
    stop -= 1
    start = None if start == 0 else start - 1
  return stop - seq[stop:start:-1].index(value)

The technique using len(seq) - 1 - next(i for i,v in enumerate(reversed(seq)) if v == value), suggested in several other answers, can be more space-efficient: it needn't create a reversed copy of the full list. But in my (offhand, casual) testing, it's about 50% slower.

1

Use a simple loop:

def reversed_index(items, value):
    for pos, curr in enumerate(reversed(items)):
        if curr == value:
            return len(items) - pos - 1
    raise ValueError("{0!r} is not in list".format(value))
1

Love @alcalde's solution, but faced ValueError: max() arg is an empty sequence if none of the elements match the condition.

To avoid the error set default=None:

max((loc for loc, val in enumerate(li) if val == 'a'), default=None)
1
  • This would go through the whole list li even after the element is found.
    – xuhdev
    Commented Jul 13 at 5:32
1

There's a lot of answers here, so I thought it'd be interesting to compare different approaches. I've also came up with a pretty good solution not listed here and created a PyPI package.

Let's get straight to the point, here's the code I've used for benchmarking:

from timeit import timeit
from itertools import dropwhile
from operator import indexOf

from rindex import rindex


def rindex1(li, value):
    return len(li) - 1 - li[::-1].index(value)


def rindex2(li, value):
    for i in reversed(range(len(li))):
        if li[i] == value:
            return i
    raise ValueError("{} is not in list".format(value))


def rindex3(li, value):
    return max(loc for loc, val in enumerate(li) if val == value)


def rindex4(li, value):
    for r_idx, elt in enumerate(reversed(li)):
        if elt == value:
            return len(li) - 1 - r_idx


def rindex5(li, value):
    return next(dropwhile(lambda x: li[x] != value, reversed(range(len(li)))))


def rindex6(li, value):
    return dict(map(reversed, enumerate(li)))[value]


def rindex7(seq, value, start=None, stop=None):
    """L.rindex(value, [start, [stop]]) -> integer -- return last index of value.
    Raises ValueError if the value is not present."""
    start, stop, _ = slice(start, stop).indices(len(seq))
    if stop == 0:
        # start = 0
        raise ValueError("{!r} is not in list".format(value))
    else:
        stop -= 1
        start = None if start == 0 else start - 1
    return stop - seq[stop:start:-1].index(value)


# Thanks Kelly Bundy for mentioning this in the comments
# https://stackoverflow.com/a/63834895
def rindex8(li, value):
    return len(li) - 1 - indexOf(reversed(li), value)


def my_rindex(li, value, start=0, stop=None, /):
    size = len(li)
    li.reverse()
    try:
        return (
            size - 1 - li.index(value, 0 if stop is None else size - stop, size - start)
        )
    finally:
        li.reverse()


FUNCTIONS = (rindex, rindex1, rindex2, rindex3, rindex4, rindex5, rindex6, rindex7, rindex8, my_rindex)
FUNCTIONS_WITH_BOUNDARIES = (rindex7, my_rindex, rindex)


def test_correctness():
    li = [0, 1, 0, 2]
    for f in FUNCTIONS:
        assert f(li, 0) == 2
    for f in FUNCTIONS_WITH_BOUNDARIES:
        assert f(li, 0, 0, 1) == 0


def test_speed():
    small = list(range(10))
    big = list(range(1_000_000))
    tests = (
        ("small best", small, len(small) - 1, 1_000_000),
        ("big best", big, len(big) - 1, 100),
        ("small middle", small, len(small) // 2, 1_000_000),
        ("big middle", big, len(big) // 2, 100),
        ("small worst", small, 0, 1_000_000),
        ("big worst", big, 0, 100),
    )
    for name, li, value, repeats in tests:
        print(format(f" {name} ", "=^22"))
        for f in (list.index,) + FUNCTIONS:
            print(
                f.__name__.ljust(10),
                ":",
                format(
                    timeit(
                        "f(li, value)",
                        globals={
                            "f": f,
                            "li": li,
                            "value": value
                            if f is not list.index
                            else len(li) - 1 - value,
                        },
                        number=repeats,
                    ),
                    "9f",
                ),
            )


if __name__ == "__main__":
    test_correctness()
    test_speed()

And the results:

===== small best =====
index      :  0.124899
rindex     :  0.168111
rindex1    :  0.711771
rindex2    :  1.042878
rindex3    :  3.031604
rindex4    :  1.018811
rindex5    :  2.309284
rindex6    :  8.428993
rindex7    :  1.536563
rindex8    :  0.714625
my_rindex  :  0.751303
====== big best ======
index      :  0.000020
rindex     :  0.000030
rindex1    :  2.520536
rindex2    :  0.000187
rindex3    : 12.694951
rindex4    :  0.000135
rindex5    :  0.000276
rindex6    : 82.994252
rindex7    :  2.820221
rindex8    :  0.000113
my_rindex  :  0.619653
==== small middle ====
index      :  0.264816
rindex     :  0.325221
rindex1    :  0.875725
rindex2    :  1.440296
rindex3    :  3.100110
rindex4    :  1.424884
rindex5    :  3.330751
rindex6    :  8.430511
rindex7    :  1.686265
rindex8    :  0.888111
my_rindex  :  0.888112
===== big middle =====
index      :  1.813371
rindex     :  1.747949
rindex1    :  4.465189
rindex2    :  5.886711
rindex3    : 12.696632
rindex4    :  6.856758
rindex5    : 13.855785
rindex6    : 84.861101
rindex7    :  4.457431
rindex8    :  2.044351
my_rindex  :  2.299312
==== small worst =====
index      :  0.452868
rindex     :  0.456035
rindex1    :  1.016495
rindex2    :  1.846306
rindex3    :  3.038056
rindex4    :  1.901025
rindex5    :  4.539291
rindex6    :  8.428476
rindex7    :  1.841232
rindex8    :  1.061607
my_rindex  :  1.054495
===== big worst ======
index      :  3.620210
rindex     :  3.083546
rindex1    :  5.824754
rindex2    : 11.764573
rindex3    : 12.696748
rindex4    : 13.481179
rindex5    : 27.432877
rindex6    : 82.762718
rindex7    :  5.880688
rindex8    :  3.685528
my_rindex  :  3.725783

Here, the "best" case means that the searched value is rightmost, "middle" - in the middle, "worst" - leftmost. Technically, the worst case would be absence of the searched value, but handling of exceptions may mess up the measurements.

rindex from my rindex package is superior (even beats the built-in index in some cases) because it's written in Cython. Use it if you need the best performance.

Comparison of pure Python functions:

We can also exclude rindex3, rindex5 and rindex6 from the competition: they do not have advantages.

rindex2 and rindex4 are almost the same, but 4 is a little faster for "good" cases, and for the "bad" ones it's the opposite. They're also excluded because rindex8 is faster in every case.

rindex1 is probably the best one for small lists (though, rindex8 and my_rindex are only a bit slower).

For other cases rindex8 is the fastest (even comparable to the built-in index in the "big worst" case).

my_rindex is almost as fast, but slows down dramatically in the "big best" case. It supports boundaries, though.

However, if you use boundaries that effectively reduce a big list to a small list, rindex7 will be your friend.

12
  • 1
    You can see another fast one here (the second one, you already got the first one). Commented Dec 4, 2023 at 18:22
  • Beating list.index is rather suspicious, makes me think something might be wrong. I see you only inserted the new times. Did you only run the new function? Then maybe your computer was for some reason faster than when you did the previous measurements. When I use your package, is there an easy way for me to tell which of the three implementations is getting used? Commented Dec 11, 2023 at 12:19
  • You have a bug. Set up x = float('nan') and xs = [x]. Then xs.index(x) gives me 0 as expected, but rindex(xs, x) gives me a ValueError, falsely claiming that the element isn't in the sequence. Commented Dec 11, 2023 at 12:27
  • The error traceback tells me that it happened in c_rindex.pyx line 29. Does that answer my earlier question, does that mean I'm running Cython? (Or am I just running your Cython code but maybe in pure Python without Cython? I'm not familiar with Cython, and I'm running it on replit.com, which might or might not have installed Cython for me.) Commented Dec 11, 2023 at 12:33
  • @KellyBundy yes, pyx means Cython. I've run the tests a lot of times, so I'm pretty sure about the results. The "bug" you've described is an interesting case. Try [float("nan")].index(float("nan")). According to Python docs, index must "Return zero-based index in the list of the first item whose value is equal to x.". But x != x if x is NaN!
    – solaluset
    Commented Dec 11, 2023 at 14:50
0
lastIndexOf = lambda array, item: len(array) - (array[::-1].index(item)) - 1
0

If the list is small, you can compute all indices and return the largest:

index = max(i for i, x in enumerate(elements) if x == 'foo')
1
  • This would go through the whole list li even after the element is found.
    – xuhdev
    Commented Jul 13 at 5:33
0
def rindex(lst, val):
  try:
    return next(
      len(lst) - n
      for n, v in enumerate(reversed(lst), start=1)
      if v == val
    )
  except StopIteration:
    raise ValueError(f'{val} is not in list')
0

Most Pythonic and efficient way and also applicable to non-string lists:

len(li) - next((i for i, e in enumerate(reversed(li)) if e == "a"), len(li) + 1)

If not found, the expression evaluates to -1.

No new list construction, 100% on iterator.

-1

val = [1,2,2,2,2,2,4,5].

If you need to find last occurence of 2

last_occurence = (len(val) -1) - list(reversed(val)).index(2)

-1

Here's a little one-liner for obtaining the last index, using enumerate and a list comprehension:

li = ["a", "b", "a", "c", "x", "d", "a", "6"]
[l[0] for l in enumerate(li) if l[1] == "a"][-1]

Not the answer you're looking for? Browse other questions tagged or ask your own question.