114

One frequently finds expressions of this type in python questions on SO. Either for just accessing all items of the iterable

for i in range(len(a)):
    print(a[i])

Which is just a clumbersome way of writing:

for e in a:
    print(e)

Or for assigning to elements of the iterable:

for i in range(len(a)):
    a[i] = a[i] * 2

Which should be the same as:

for i, e in enumerate(a):
     a[i] = e * 2
# Or if it isn't too expensive to create a new iterable
a = [e * 2 for e in a]

Or for filtering over the indices:

for i in range(len(a)):
    if i % 2 == 1: continue
    print(a[i])

Which could be expressed like this:

for e in a [::2]:
    print(e)

Or when you just need the length of the list, and not its content:

for _ in range(len(a)):
    doSomethingUnrelatedToA()

Which could be:

for _ in a:
    doSomethingUnrelatedToA()

In python we have enumerate, slicing, filter, sorted, etc... As python for constructs are intended to iterate over iterables and not only ranges of integers, are there real-world use-cases where you need in range(len(a))?

Hyperboreus
  • 31,997
  • 9
  • 47
  • 87
  • 9
    I think `range(len(a))` is usually people who are fairly inexperienced with Python (although not necessarily with programming in general). – rlms Oct 04 '13 at 14:54
  • 1
    I only used `range(len(a))` when I was learning Python. Nowadays, I don't because, as you stated, it's quite easy to replace. –  Oct 04 '13 at 14:56
  • 4
    not really. I use `range(len(a))` often, because i don't need the content of list a, but only the length. – aIKid Oct 04 '13 at 14:59
  • @aIKamili - That is one use of `range(len(a))`. However, even that can be replaced by: `for _ in a:`. The code I gave will only iterate for as many times as there are items in list `a`. –  Oct 04 '13 at 15:02
  • @alKamili I'd personally use `enumerate` or `for _in list_` instead, but I guess in that case it is mostly just personal preference. – rlms Oct 04 '13 at 15:03
  • 12
    What if in the loop I need to access the element before and after the current one? I usually have `for i in range(len(a)): doSomethingAbout(a[i+1] - a[i])` How to get around that? – Zhang18 Jan 18 '17 at 18:34
  • @Zhang18 I think your example gives an IndexError. If you have for example `a=range(10)` then your loops goes from `doSomethingAbout(a[1] - a[0])` to `doSomethingAbout(a[10] - a[9])` but there is no element `a[10]`. You can slice the list and then iterate over the sliced list: `a = range(10) for i in a[:-1]: print(a[i+1]-a[i])` – Jaakko Seppälä Jun 21 '18 at 10:25
  • 1
    @JaakkoSeppälä agreed. I was just giving an example to illustrate the main issue of having to loop through indices, not just values, understanding there is a corner case at the end which is besides the main point. – Zhang18 Jun 25 '18 at 15:01
  • Should be an use case here: https://stackoverflow.com/questions/59112685/how-to-multiply-each-integer-one-by-one-and-display-result-in-progressive-ord – ytu Nov 30 '19 at 02:53
  • This is a great question. – Roly May 27 '21 at 15:47
  • 1
    My suspicion: Old-fashioned tutorials are stupid and teach the `range(len(list))` solution when they should be using enumerate as the starting point! Just suffered through 30 mins of trying to explain the `range(len(list))` chorizo to someone I'm tutoring, just because that's what Codecademy decided to burden us with. Why??? Obscure excuses aside, the default should be `enumerate()` but `range(len(list))` persists. This standard makes PHP's `foreach()` look good, for goodness sake (^_^) – jerclarke Aug 08 '21 at 23:21
  • FWIW `enumerate()` was only added to Python in 2003 (https://www.python.org/dev/peps/pep-0279/), 3 years after Python 2 came out and 12 years after the original release in 1991. I suspect that `range(len(list))` was the standard for so long that it's just hard to get rid of. – jerclarke Aug 08 '21 at 23:23

15 Answers15

19

If you need to work with indices of a sequence, then yes - you use it... eg for the equivalent of numpy.argsort...:

>>> a = [6, 3, 1, 2, 5, 4]
>>> sorted(range(len(a)), key=a.__getitem__)
[2, 3, 1, 5, 4, 0]
Jon Clements
  • 138,671
  • 33
  • 247
  • 280
  • OK, this looks sensible. Thank you very much. But the question is: what will you do with your newly sorted list of indices. If via this list again you access some iterable, the dog bites its own tail. – Hyperboreus Oct 04 '13 at 15:15
  • 2
    Equivalent to: `[ix for ix, _ in sorted(enumerate(a), key=lambda i: i[1])]` though, although yours is arguably nicer/geekier. – Erik Kaplun Oct 04 '13 at 15:21
12

Short answer: mathematically speaking, no, in practical terms, yes, for example for Intentional Programming.

Technically, the answer would be "no, it's not needed" because it's expressible using other constructs. But in practice, I use for i in range(len(a) (or for _ in range(len(a)) if I don't need the index) to make it explicit that I want to iterate as many times as there are items in a sequence without needing to use the items in the sequence for anything.

So: "Is there a need?"? — yes, I need it to express the meaning/intent of the code for readability purposes.

See also: https://en.wikipedia.org/wiki/Intentional_programming

And obviously, if there is no collection that is associated with the iteration at all, for ... in range(len(N)) is the only option, so as to not resort to i = 0; while i < N; i += 1 ...

Erik Kaplun
  • 37,128
  • 15
  • 99
  • 111
  • What advantages has `for _ in range(len(a))` over `for _ in a`? – Hyperboreus Oct 04 '13 at 15:11
  • 1
    @Hyperboreus: yeah, I just amended my answer a few seconds before your comment... so I guess the difference is whether you want to be really explicit about **"repeat AS MANY TIMES as there are items in `a`"** as opposed to **"for every element in `a`, regardless of the content of `a`"**... so just an Intentional Programming nuance. – Erik Kaplun Oct 04 '13 at 15:13
  • Thank you for your example. I've included it in my question. – Hyperboreus Oct 04 '13 at 15:14
  • 4
    To get a list of `'hello'` with as many items as in list `a`, use `b = ['hello'] * len(a)` – steabert Nov 24 '15 at 09:59
11

What if you need to access two elements of the list simultaneously?

for i in range(len(a[0:-1])):
    something_new[i] = a[i] * a[i+1]

You can use this, but it's probably less clear:

for i, _ in enumerate(a[0:-1]):
     something_new[i] = a[i] * a[i+1]

Personally I'm not 100% happy with either!

Giswok
  • 559
  • 5
  • 16
  • 1
    `for ix, i in enumerate(a)` seems to be equivalent, no? – Erik Kaplun Jul 29 '18 at 14:59
  • 7
    One should use [pairwise](https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.pairwise) instead. – flying sheep Nov 05 '19 at 12:11
  • 11
    In those situations I do: `for a1,a2 in zip(a[:-1],a[1:])` – Luca Feb 11 '20 at 16:17
  • 1
    zip is the preferred method. Please use zip. – stidmatt Jan 31 '22 at 21:14
  • 1
    @stidmatt Why would zip be the preferred method when it is much less readable? – Joshua P. Swanson Jun 10 '22 at 06:08
  • @JoshuaP.Swanson `zip` is considerably more readable once you become familiar with the language, as brevity is one of the core tenets of Python. Happy coding! – iteratedwalls Sep 22 '22 at 21:56
  • @iteratedwalls I am very familiar with the language. I have two issues with `zip` here. First, the names `a1, a2` gives no indication that they're just offset by 1. `a[i], a[i+1]` makes this clear. Second, you have to reason through the list slices and figure out exactly what's getting combined and how--put a semicolon or negative in the wrong place and it means something else, generally nonsense. It's certainly cute, but it's not a common enough operation to be idiomatic. Calling it "the preferred method" suggests there's really no argument against it, which is just wrong depending on context – Joshua P. Swanson Sep 23 '22 at 02:11
  • Please see: [How can I iterate over overlapping (current, next) pairs of values from a list?](https://stackoverflow.com/questions/5434891/) – Karl Knechtel Jul 30 '23 at 05:36
2

Going by the comments as well as personal experience, I say no, there is no need for range(len(a)). Everything you can do with range(len(a)) can be done in another (usually far more efficient) way.

You gave many examples in your post, so I won't repeat them here. Instead, I will give an example for those who say "What if I want just the length of a, not the items?". This is one of the only times you might consider using range(len(a)). However, even this can be done like so:

>>> a = [1, 2, 3, 4]
>>> for _ in a:
...     print True
...
True
True
True
True
>>>

Clements answer (as shown by Allik) can also be reworked to remove range(len(a)):

>>> a = [6, 3, 1, 2, 5, 4]
>>> sorted(range(len(a)), key=a.__getitem__)
[2, 3, 1, 5, 4, 0]
>>> # Note however that, in this case, range(len(a)) is more efficient.
>>> [x for x, _ in sorted(enumerate(a), key=lambda i: i[1])]
[2, 3, 1, 5, 4, 0]
>>>

So, in conclusion, range(len(a)) is not needed. Its only upside is readability (its intention is clear). But that is just preference and code style.

  • Thank you very much. And then again, readability is (partially) in the eye of the beholder. I interpret `for _ in a:` as "Iterate over a but ignore its content", but I interpret `for _ in range(len(a))` as "Get the length of a, then create a number of integers of the same length, and then finally ignore the content". – Hyperboreus Oct 04 '13 at 15:36
  • 2
    @Hyperboreus - Very true. It's just code style. My goal was to show there will never be a "I must use `range(len(a))` or I can't do this" scenario. –  Oct 04 '13 at 15:38
  • 1
    On a side note: E.g. in erlang the single underscore is the anonymous variable. It is the only variable which can be re-assigned (or "matched"), unlike other variables, as erlang doesn't allow destructive assignment (which generally speaking is an abomination and weakens the veil between us and the nether realms, where HE waits behind the wall in his palace built of tortured glass). – Hyperboreus Oct 04 '13 at 19:04
2

Sometimes matplotlib requires range(len(y)), e.g., while y=array([1,2,5,6]), plot(y) works fine, scatter(y) does not. One has to write scatter(range(len(y)),y). (Personally, I think this is a bug in scatter; plot and its friends scatter and stem should use the same calling sequences as much as possible.)

2

It's nice to have when you need to use the index for some kind of manipulation and having the current element doesn't suffice. Take for instance a binary tree that's stored in an array. If you have a method that asks you to return a list of tuples that contains each nodes direct children then you need the index.

#0 -> 1,2 : 1 -> 3,4 : 2 -> 5,6 : 3 -> 7,8 ...
nodes = [0,1,2,3,4,5,6,7,8,9,10]
children = []
for i in range(len(nodes)):
  leftNode = None
  rightNode = None
  if i*2 + 1 < len(nodes):
    leftNode = nodes[i*2 + 1]
  if i*2 + 2 < len(nodes):
    rightNode = nodes[i*2 + 2]
  children.append((leftNode,rightNode))
return children

Of course if the element you're working on is an object, you can just call a get children method. But yea, you only really need the index if you're doing some sort of manipulation.

CleoR
  • 806
  • 6
  • 18
2

Sometimes, you really don't care about the collection itself. For instance, creating a simple model fit line to compare an "approximation" with the raw data:

fib_raw = [1, 1, 2, 3, 5, 8, 13, 21] # Fibonacci numbers

phi = (1 + sqrt(5)) / 2
phi2 = (1 - sqrt(5)) / 2

def fib_approx(n): return (phi**n - phi2**n) / sqrt(5)

x = range(len(data))
y = [fib_approx(n) for n in x]

# Now plot to compare fib_raw and y
# Compare error, etc

In this case, the values of the Fibonacci sequence itself were irrelevant. All we needed here was the size of the input sequence we were comparing with.

Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
1

If you have to iterate over the first len(a) items of an object b (that is larger than a), you should probably use range(len(a)):

for i in range(len(a)):
    do_something_with(b[i])
alexpirine
  • 3,023
  • 1
  • 26
  • 41
0

I have an use case I don't believe any of your examples cover.

boxes = [b1, b2, b3]
items = [i1, i2, i3, i4, i5]
for j in range(len(boxes)):
    boxes[j].putitemin(items[j])

I'm relatively new to python though so happy to learn a more elegant approach.

Jim
  • 17
  • 3
  • 4
    My ignorance. There's zip, a much more pythonic way of iterating over 2 lists in parallel. – Jim Apr 29 '15 at 23:03
  • 1
    Hah, I came here with a really similar use case... `[a - b for a, b in zip(list1, list2)]` is so much nicer than `[list1[i] - list2[i] for i in range(len(list1))]`.. Thanks! – kevlarr Feb 17 '17 at 17:49
0

Very simple example:

def loadById(self, id):
    if id in range(len(self.itemList)):
        self.load(self.itemList[id])

I can't think of a solution that does not use the range-len composition quickly.

But probably instead this should be done with try .. except to stay pythonic i guess..

IARI
  • 1,217
  • 1
  • 18
  • 35
0

One problem with for i, num in enumerate(a) is that num does not change when you change a[i]. For example, this loop:

for i, num in enumerate(a):
    while num > 0:
        a[i] -= 1

will never end. Of course, you could still use enumerate while swapping each use of num for a[i], but that kind of defeats the whole purpose of enumerate, so using for i in range(len(a)) just becomes more logical and readable.

0

Having a range of indices is useful for some more sophisticated problems in combinatorics. For example, to get all possible partitions of a list into three non-empty sections, the most straightforward approach is to find all possible combinations of distinct endpoints between the first and second section and between the second and third section. This is equivalent to ordered pairs of integers chosen from the valid indices into the list (except zero, since that would make the first partition empty). Thus:

>>> from itertools import combinations
>>> def three_parts(sequence):
...     for i, j in combinations(range(1, len(sequence)), 2):
...         yield (sequence[:i], sequence[i:j], sequence[j:])
... 
>>> list(three_parts('example'))
[('e', 'x', 'ample'), ('e', 'xa', 'mple'), ('e', 'xam', 'ple'), ('e', 'xamp', 'le'), ('e', 'xampl', 'e'), ('ex', 'a', 'mple'), ('ex', 'am', 'ple'), ('ex', 'amp', 'le'), ('ex', 'ampl', 'e'), ('exa', 'm', 'ple'), ('exa', 'mp', 'le'), ('exa', 'mpl', 'e'), ('exam', 'p', 'le'), ('exam', 'pl', 'e'), ('examp', 'l', 'e')]
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
0

I don't understand why no one mentioned its usefulness for keeping track of the element's index without having to add a counter inside the loop.

lst = ["a", "b", "c"]
for i in range(len(lst)):
    print(f"Current element: \"{lst[i]}\". Element index: \"{i}\".")

'''
Output:
Current element: "a". Element index: "0".
Current element: "b". Element index: "1".
Current element: "c". Element index: "2".
'''

Edit: this can also be done with enumerate:

lst = ["a", "b", "c"]
for i, elem in enumerate(lst):
    print(f"Current element: \"{elem}\". Element index: \"{i}\".")

Same output.

Gabriel
  • 1
  • 1
-1

My code is:

s=["9"]*int(input())
for I in range(len(s)):
    while not set(s[I])<=set('01'):s[i]=input(i)
print(bin(sum([int(x,2)for x in s]))[2:])

It is a binary adder but I don't think the range len or the inside can be replaced to make it smaller/better.

-1

I think it's useful for tqdm if you have a large loop and you want to track progress. This will output a progress bar:

from tqdm import tqdm

empty_list = np.full(len(items), np.nan)
for i in tqdm(range(len(items))):
    empty_list[i] = do_something(items[i])

This will not show progress, at least in the case I was using it for:

empty_list = np.full(len(items), np.nan)
for i, _ in tqdm(enumerate(items)):
        empty_list[i] = do_something(items[i])

Just showed number of iterations. Not as helpful.

maddie
  • 1