Codementor Events

Stop using indices!

Published Jul 10, 2019

A very common things I see among my newer Python students is that often try to access values by index within loops. Part of this is down to experience in other programming languages, where this kind of pattern is common, but there are also situations where they just don't realise there's a better way. In this post, I want to show off some of those better ways so you can write more Pythonic loops, and ditch indices in favour of descriptive variable names.

range and len inside for loops

How many of you have written code like this before?

players = ["John", "Anne", "Hannah"]

for i in range(len(player)):
  print(players[i])

It makes sense, right? We find out the length of the player list, create a range object containing a collection of indices, and then we iterate over that collection. We then access the names in players using those indices.

The problem is, we didn't need to do any of that stuff. Python uses a for else loop, which is a little bit different from what we might think of as a for loop in other languages.

Unlike a for loop, a for else loop gives us direct access to the items in a collection. We can therefore assign those items directly to the loop variable, instead of faffing around like we did in the example above.

A more Pythonic way of writing the loop in the example might look like this:

for player in players:
  print(player)

Here instead of i, we've got a nice descriptive variable name, and the code is overall way shorter and less complex.

Just for completeness Python comprehensions work in exactly the same way, so anything mentioned in this post is also applicable to those structures.

However, this is a pretty simple example, so let's move onto something a little more involved.

Working with multiple collections

In the Python course for which I help mentor, we have an exercise where students code up a kind of lottery.

Today I reviewed one our students' solutions, and I found a loop which made a lot of sense, but wasn't really done in a Pythonic style, and was way more complicated than it needed to be as a result.

As part of their solution, the student had a list of dictionaries which contained all the player names and their lottery numbers, which looked like this:

players = [
    {'name': 'Rolf', 'numbers': {1, 3, 5, 7, 9, 11}},
    {'name': 'Charlie', 'numbers': {2, 7, 9, 22, 10, 5}},
    {'name': 'Anna', 'numbers': {13, 14, 15, 16, 17, 18}},
    {'name': 'Jen', 'numbers': {19, 20, 12, 7, 3, 5}}
]

They'd compared these numbers to a randomly selected set of 6 numbers using an set intersection, and they'd put the number of matching numbers for each player in a list called count_correct_list.

They then found out what the highest number of matching numbers was using max, and stored the result in max_correct.

So far, so good.

Finally, they came to checking the amount of correct numbers for each player against this max_correct value, so that they could print out all of the winning players and their winnings. Here is how they did it:

for i in range(len(players)):
    if count_correct_list[i] == max_correct:
        print(f"{players[i]['name']} won {100 ** max_correct}")

Again, this makes a lot of sense. We need to access both the count_correct_list and the players list in the same loop, so if we generate a list of indices, we can rely on the stable ordering of lists to access the correct values in both collections.

But again, we don't need to do any of this. Instead, we can make use of zip. zip is legitimately amazing. It essentially allows us to combine two or more iterables into a single zip object. The first value of each iterable gets collected up into a tuple, then the second item gets put in a second tuple, and so on, and so forth.

So, instead of doing all this work with indices, let's create a zip object where we combine the two collections we're trying to access:

for player, amount_correct in zip(players, count_correct_list):
    if amount_correct == max_correct:
        print(f"{player['name']} won {100 ** amount_correct}.")

As you can see above, we can destructure the zip object tuples into the loop variables, and now we can access the values from both collections using good, descriptive names. Way better than count_correct_list[i].

If you're not using zip, you need to start using zip.

Using enumerate

Another instance where I often see students resorting to indices, is when they want to have some kind of counter alongside a given value, and they think they can kill two birds with one stone by generating the counter, and then using the counter to access the values.

A better way to do this is to use the enumerate function. enumerate returns an enumerate object containing a series of tuples. The first value in each tuple is a counter, which by default starts at 0. The second item in the tuple is some value from an iterable object we pass to the function.

Let's look at an example where we print a number alongside the name of a player.

players = ["John", "Anne", "Hannah"]

for counter, player in enumerate(players):
  print(f"{counter}: {player}")

The output would look like this:

0: John
1: Anne
2: Hannah

If we wanted the counter to start from 1 instead of 0, we could specify a value for the start parameter like so:

for counter, player in enumerate(players, start=1):

Conclusion

I hope you learnt something new and are inspired to try the techniques I showed off here. Mastering these tools is really vital to writing awesome Pythonic loops, but these tools can also be used elsewhere to make your code more explicit and readable.

Next time you use an index in a loop, just take a moment to think, "Is there a better way to do this?" More often than not, the answer is, "Yes, absolutely."

Discover and read more posts from Phil Best
get started
post commentsBe the first to share your opinion
Tulsi Prasad
5 years ago

To be honest, “zip is legitimately amazing” is totally right :)

Tulsi Prasad
5 years ago

This was really helpful @Phil. Being a beginner in Python myself I’ve written code in a less pythonic way earlier, and now that you’ve said it, it’s easier for us. Great job, man!

Show more replies