Python Loop with Index: Using enumerate() and Alternatives
python loop with index: Learn how to loop with an index in Python using enumerate(), range(len()), and alternatives, with practical examples and performance notes.
python loop with index requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you need both the element and its position while iterating, Python offers several ways to loop with an index. The most readable and Pythonic is enumerate(), but you will still see range(len()) in older codebases. This article compares the approaches, explains their behavior, and gives concrete guidance on when to use each.
The Idiomatic Approach: enumerate()
The built-in enumerate() function is designed exactly for this task. It takes an iterable and returns an iterator that yields pairs of (index, value) as you loop. The index starts at 0 by default, but you can change that with the start parameter.
colors = ["red", "green", "blue"] for index, color in enumerate(colors): print(index, color)
Output:
0 red
1 green
2 blue
The unpacking in the for statement is what makes this so readable. You get both the position and the element without manually indexing the collection. If you need the first element to have index 1, pass start=1:
for index, color in enumerate(colors, start=1): print(index, color)
This is the recommended way to loop with an index in Python because it is explicit, concise, and works with any iterable, not just sequences.
Looping with range(len()) and Why It's Discouraged
Before enumerate() became the standard idiom, developers often wrote loops like this:
for i in range(len(colors)): print(i, colors[i])
This works, but it has several drawbacks. First, it only works on sequences that support indexing, such as lists, tuples, and strings. If you try it on a set or a generator, you get a TypeError because those types do not support len() or indexing. Second, it is less readable because you have to write colors[i] every time you want the value. The intent is buried under the indexing mechanics. Finally, it is easier to make off-by-one errors, especially when the collection is empty or when you modify the collection inside the loop.
While range(len()) is not wrong in every situation, it is rarely the best choice. Reserve it for cases where you genuinely need to modify elements by index and you are certain the object is a sequence.
Alternatives for Special Cases
Sometimes you need to iterate over two collections in parallel, and you also need an index. The zip() function can help, but you still need a counter. A common pattern is:
names = ["Alice", "Bob", "Charlie"] scores = [85, 92, 78] for i, (name, score) in enumerate(zip(names, scores)): print(i, name, score)
Here enumerate() wraps the zip iterator, giving you an index for each pair. If you need to iterate over a collection and also keep a manual counter, a while loop with an explicit variable is possible, but it is more verbose and error-prone:
i = 0 while i < len(colors): print(i, colors[i]) i += 1
This pattern is rarely necessary. enumerate() covers the vast majority of use cases where you need both the index and the value.
Performance and Memory Considerations
Both enumerate() and range(len()) are lazy in the sense that they do not create a list of all indices or pairs in memory. enumerate() returns an iterator that yields one tuple at a time, while range() returns a range object that generates integers on demand. For a list, indexing with colors[i] is O(1), so the performance difference between the two approaches is negligible for most workloads.
The real performance concern is when you materialize the enumeration into a list. For example, list(enumerate(colors)) creates a new list of tuples, which consumes memory proportional to the length of the collection. If you only need to iterate once, avoid this conversion. Similarly, range(len(colors)) does not create a list, so it is memory-efficient, but it forces you to index the original collection repeatedly, which is an extra operation per iteration.
For large collections, the difference is still small, but enumerate() is more readable and avoids the risk of accidentally using a non-indexable iterable.
Modifying the Collection While Iterating
A common task is to update elements in a list based on their index. Using enumerate() is safe when you modify elements in place without changing the list length:
numbers = [1, 2, 3, 4] for i, n in enumerate(numbers): if n % 2 == 0: numbers[i] = n * 10
This works because you are only assigning to existing positions. However, if you try to remove elements from the list while iterating, you will skip items because the list shrinks and the index shifts. A common workaround is to iterate over a copy:
for i, n in enumerate(numbers[:]): if n % 2 == 0: numbers.remove(n)
But this is inefficient and error-prone. A better approach is to use a list comprehension to create a new list:
numbers = [n for n in numbers if n % 2 != 0]
When you need to modify the list by index, enumerate() gives you the index you need. When you need to filter or transform, prefer a list comprehension or generator expression.
Choosing the Right Approach for Your Code
The decision between enumerate() and range(len()) comes down to what you need and what type of iterable you are working with.
Use enumerate() when:
- You need both the index and the value.
- You are iterating over any iterable, including generators, sets, or custom objects.
- You want to avoid manual indexing and keep the code readable.
Use range(len()) only when:
- You are certain the object is a sequence with a defined length and supports indexing.
- You need to modify elements at specific positions and the modification does not change the collection size.
- You are working in a codebase that already uses this style and you want to stay consistent.
For parallel iteration over multiple collections, combine enumerate() with zip(). For simple counting without accessing the collection, use range() directly.
Common Mistakes and How to Avoid Them
One frequent mistake is forgetting to unpack the tuple from enumerate(). Writing for i in enumerate(colors) gives you a tuple (index, value) in i, not the index alone. Always use two variables in the for statement, or access i[0] and i[1] explicitly if you really need a tuple.
Another mistake is using range(len()) on an iterable that does not support indexing. For example, a set or a generator will raise a TypeError. If you are not sure whether the object is a sequence, enumerate() is safer because it works with any iterable.
Off-by-one errors are common when using start with enumerate(). If you want the first index to be 1, pass start=1; do not manually add 1 inside the loop. That leads to confusing code and potential errors when the collection is empty.
Finally, be careful when modifying a collection while iterating over it. Changing the length of a list during iteration can cause skipped elements or an IndexError. If you need to remove items, iterate over a copy or use a list comprehension. If you need to update values, enumerate() gives you the index to assign directly.