[go: up one dir, main page]

Avoid indexes in Python

Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
6 min. read Python 3.10—3.14
Share
Copied to clipboard.
Tags

When writing Python code, I recommend avoiding indexes when reasonably possible.

Indexing isn't bad, but there's often a higher level way to accomplish a goal than using an index.

In particular, in Python we tend not to use indexes for:

  • Substring checks and other containment checks
  • Picking specific items from tuples (or other fixed-length iterables)
  • Looping over lists
  • Looping over multiple lists
  • Looping in over lists in reverse

Avoiding indexes with containment checks

Here are two strings:

>>> phrase = "PyCon US"
>>> message = "I was at PyCon US a couple weeks ago."

We'd like to see whether the first string is a substring of the second:

We could use the string find method:

>>> contains_phrase = message.index(phrase) != -1
True

But instead we prefer to use the in operator:

>>> contains_phrase = phrase in message
True

Python has a special operator that's meant just for checking whether one thing is contained inside another. With strings it does exactly the substring check we're looking for.

Of Python's many string methods, the find method is quite rarely used. Unless you really need an index for your substring match, a containment check will do just fine.

Tuple unpacking versus hard-coded indexes

In Python, we tend to avoid hard-coded indexes when possible (e.g. coordinates[2]).

Hard-coded indexes give a number to an object. It's often more readable to give a name to an object instead just a number.

So instead of using indexes to get each item within a tuple:

from shutil import get_terminal_size

size = get_terminal_size()
print(f"Dimensions: {size[0]} x {size[1]}")

We tend to use tuple unpacking:

from shutil import get_terminal_size

width, height = get_terminal_size()
print(f"Dimensions: {width} x {height}")

For more on how tuple unpacking improves readability, see this article.

Looping without indexes

Python's for loops don't use indexes.

Python's range function can be used to get indexes from a sequence, loop over those indexes, and then index the sequence to get each item:

>>> fruits = ["apples", "oranges", "bananas", "strawberries", "pears"]
>>> for i in range(len(fruits)):
...     print(fruits[i])
...
apples
oranges
bananas
strawberries
pears

But that's adding extra steps that we don't need. Python's for loops will provide each item from any iterable as we loop.

>>> fruits = ["apples", "oranges", "bananas", "strawberries", "pears"]
>>> for fruit in fruits:
...     print(fruit)
...
apples
oranges
bananas
strawberries
pears

The index is just one way to access an item. Usually it's the item we actually want, not the index.

Looping over multiple lists without indexes

What if we need to loop over multiple lists (or other sequences) at once?

>>> colors = ["purple", "pink", "blue"]
>>> animals = ["duck", "flamingo", "unicorn"]

Do we need to fall back to using indexes?

>>> for i in range(len(colors)):
...     print(colors[i], animals[i])
...
purple duck
pink flamingo
blue unicorn

We don't!

Python's zip function was made for exactly this use case:

>>> colors = ["purple", "pink", "blue"]
>>> animals = ["duck", "flamingo", "unicorn"]
>>> for color, animal in zip(colors, animals):
...     print(color, animal)
...
purple duck
pink flamingo
blue unicorn

Looping in reverse

What if we need to loop a list from the last item to the first item?

We could use indexes:

>>> colors = ["purple", "pink", "blue"]
>>> for i in range(len(colors), 0, -1):
...     print(colors[i-1])
...
blue
pink
purple

Or we could slice the list to make a reversed copy and then loop over that. Although, using indexes would avoid copying the whole list just to loop over it once.

Fortunately, there's an even better way to loop in reverse that's more readable than either indexing or slicing!

The built-in reversed function can loop in reverse for us:

>>> colors = ["purple", "pink", "blue"]
>>> for color in reversed(colors):
...     print(color)
...
blue
pink
purple

The reversed function works an any sequence and any other reversible iterable (dictionaries for example).

Other looping helpers to help avoid indexing

Need to peek at the next item in a list while looping?

>>> days = ["Mon", "Tues", "Wed", "Thurs", "Fri", "Sat", "Sun"]

Why use indexes?

>>> for i, day in enumerate(days):
...     if i+1 < len(days):
...         print(f"{day} today, {days[i+1]} tomorrow")
...
Mon today, Tues tomorrow
Tues today, Wed tomorrow
Wed today, Thurs tomorrow
Thurs today, Fri tomorrow
Fri today, Sat tomorrow
Sat today, Sun tomorrow

When we could use itertools.pairwise (added in Python 3.10):

>>> from itertools import pairwise
>>> for day, next_day in pairwise(days):
...     print(f"{day} today, {next_day} tomorrow")
...
Mon today, Tues tomorrow
Tues today, Wed tomorrow
Wed today, Thurs tomorrow
Thurs today, Fri tomorrow
Fri today, Sat tomorrow
Sat today, Sun tomorrow

Need to loop 2 items at a time? We don't need indexes for that either.

We need itertools.batched (added in Python 3.12):

>>> from itertools import batched
>>> names = ["Gary", "Ella", "Tom", "Allen", "Lisa", "Helen"]
>>> for name1, name2 in batched(names, 2):
...     print(name1, "and", name2)
...
Gary and Ella
Tom and Allen
Lisa and Helen

Python's itertools module includes many looping helpers for common iteration scenarios.

If you need other looping helpers, you can use third-party modules, like more-itertools.

Inventing your own looping helpers

What if you can't find the looping helper you need? Well, you could invent your own looping helper with a generator function.

For example, here's a with_next generator function:

def with_next(iterable):
    """Return each item with the item after it (None after the last)."""
    iterator = iter(iterable)
    a = next(iterator, None)
    for b in iterator:
        yield a, b
        a = b
    yield a, None

This with_next helper function works like pairwise, but it includes None after the last item:

>>> numbers = [2, 1, 3, 4, 7]
>>> for n, m in with_next(numbers):
...     print(f"{n} is followed by {m}.")
...
2 is followed by 1.
1 is followed by 3.
3 is followed by 4.
4 is followed by 7.
7 is followed by None.

Sometimes you can even get away with composing existing generator functions or other iterator-returning functions instead of making your own generator function.

For example, that with_next function could be implemented by combining pairwise with chain:

from itertools import chain, pairwise


def with_next(iterable):
    """Return each item with the item after it (None after the last)."""
    return pairwise(chain(iterable, [None]))

If you find yourself indexing sequences while looping in a similar way for more than one loop, I would consider using a generator function to make your own looping helper.

For more on making generator functions in Python, see creating generator functions.

We sometimes need to loop while counting

There are sometimes cases where we'd like to count upward while looping, either for the purpose of indexing or for another purpose. Python's enumerate function can help with that:

>>> names = ["Gary", "Ella", "Tom", "Allen", "Lisa", "Helen"]
>>> for n, name in enumerate(names, start=1):
...     print(f"{n}. {name}")
...
1. Gary
2. Ella
3. Tom
4. Allen
5. Lisa
6. Helen

But we should always ask ourselves "why are we indexing here"?

When you see an index, ask "do I need this?"

Indexes aren't bad, but well-named variables are often easier to think about than numbers.

Code with hard-coded indexes can often be replaced with tuple unpacking, which is usually more readable. Looping logic involving indexing can also often be improved by reaching for a looping helper that serves the same purpose as your indexing.

When you find yourself using indexes in Python, ask yourself is there a specialized tool for this task that might avoid indexing and make my code more readable? For many common tasks the answer might be "yes there is!"

A Python Tip Every Week

Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.