Looping Over Lists in Python#

When students new to Python are first introduced to lists, a fair amount of chaos tends to ensue when they try to loop over lists. This is understandable, as Python offers to very different paradigms for looping that are easy to get mixed up.

In this reading, we discuss these two paradigms, when you might want to use one or the other, and common pitfalls.

Iterators#

In its effort to be concise and readable, many collections in Python — like lists — understand that they may be asked to iterate over their contents one item at a time. To do so, one need only put them in a for loop after in, as follows:

my_list = [42, -3, "Apple"]
for i in my_list:
    print(i)
42
-3
Apple

This syntax is nice because you don’t have to worry about the length of the list or keeping track of which entry is which — Python will just feed you the entries of my_list one at a time.

But it is not the only way to loop.

Looping through Indices#

The other common method of looping through a list is to create a loop that iterates over the indices of the list using range(). In this case, your loop variable (the i in for i in..., or the x in for x in...) takes on integers one at a time:

for i in range(3):
    print(i)
0
1
2

We can then use these integers to extract entries from our list using [] notation:

for i in range(3):
    print(my_list[i])
42
-3
Apple

Now, commonly, if you plan to loop over items in a list, you will not type a number into range() but rather put in range(len(my_list)):

for i in range(len(my_list)):
    print(my_list[i])
42
-3
Apple

As you can see, either of these will allow you to iterate over the contents of a list. The first syntax (for i in my_list:) is a little more concise, but it just returns the contents of your list. If you think you might want to keep track of which item you’re working with, though, then you’re more likely to want to use the range() syntax. For example, if I wanted to print my progress, it’s easier with the for i in range(len(my_list)): syntax:

for i in range(len(my_list)):
    print(f"The {i} item of my list is {my_list[i]}")
The 0 item of my list is 42
The 1 item of my list is -3
The 2 item of my list is Apple

How range() Works#

It’s worth pausing for a moment to make sure we’re all clear on how range() operates. Otherwise, range(len(my_list)) can seem like a magical incantation.

Basically, range() is just a tool to automate counting between two numbers.

If you only pass one argument to range() (e.g., range(3)), then it will start at 0 and count up to (but won’t include) the number given:

for i in range(3):
    print(i)
0
1
2

Note that because you are starting with zero, you get three items if your write range(3), five if you write range(5), etc., even though range() won’t return the actual number 3 or 5.

If you pass two arguments, then it will start at the first argument given (instead of starting at 0) and count up to (but won’t return) the second argumetn:

for i in range(1, 4):
    print(i)
1
2
3

And that’s why we pass len(my_list) to range() — range wants to know what to count up to, and if our list has 10 entries, we want it to count up to len(my_list) which is 10.

range() is an iterator#

OK, now to come full circle. As I mentioned above, many things in Python — like lists — understand how to return their contents one item at a time when placed in a for loop. Well, when we write for i in range(5):, we’re actually making use of that functionality! It’s just that instead of asking a list to give us its contents one at a time, we’re asking range() to give us its contents one at a time! Those contents just happen to be sequential integers.