Back to Blog
Python

Python enumerate vs range: When to Use Each

python enumerate vs range: Understand the difference between Python's enumerate and range for indexed iteration, and learn which to use for readable, efficient loops.

Pythonenumeraterangeiterationloops
Illustration comparing Python's enumerate and range functions for loop iteration with index and value.

When Python developers compare python enumerate vs range, the decision usually comes down to whether you need the index, the value, or both. Both functions are used for looping, but they solve different problems. range produces a sequence of integers that you can use to index into a collection. enumerate yields pairs of index and value directly from an iterable. Understanding the distinction helps you write loops that are more readable, less error-prone, and sometimes faster.

The Core Difference: Indexing vs. Iteration

range is a lazy sequence generator. It produces numbers on demand, which you can use to access elements by their position. enumerate is a wrapper around an iterator that adds a counter to each item. The fundamental difference is that range gives you indices to look up values, while enumerate gives you both the index and the value in one step.

Consider a simple list:

fruits = ["apple", "banana", "cherry"]

Using range:

for i in range(len(fruits)): print(i, fruits[i])

Using enumerate:

for i, fruit in enumerate(fruits): print(i, fruit)

Both produce the same output, but the enumerate version is more direct. It does not require you to index back into the list, which makes the code easier to read and less likely to introduce an off-by-one error.

What range Does and When It Shines

range is the right tool when you need to work with indices themselves, not the values they point to. For example, if you want to modify a list in place, you need the index to assign a new value:

numbers = [1, 2, 3, 4] for i in range(len(numbers)): numbers[i] = numbers[i] * 2

Here, enumerate would also work:

for i, n in enumerate(numbers): numbers[i] = n * 2

But the range version makes it explicit that you are modifying the list by position. When you need to compare adjacent elements, range is often clearer:

for i in range(len(nums) - 1): if nums[i] > nums[i + 1]: print("Out of order")

enumerate would require you to track the next index manually, which is more awkward.

range also supports a step argument, which lets you skip elements:

for i in range(0, len(nums), 2): print(nums[i])

This is not directly possible with enumerate unless you filter the index, which adds complexity.

What enumerate Does and Why It Reads Better

enumerate is designed for the common pattern of iterating over an iterable while also needing a counter. It works with any iterable, not just sequences. For example, you can use it on a generator:

def countdown(n): while n > 0: yield n n -= 1 for i, value in enumerate(countdown(3)): print(i, value)

This would be impossible with range because generators do not support indexing. enumerate also lets you specify a starting index:

for i, line in enumerate(lines, start=1): print(f"{i}: {line}")

This is a common pattern for numbering lines in a file or items in a report.

The readability benefit is significant. When you see for i, value in enumerate(iterable), you immediately know that i is the index and value is the item. With range(len(iterable)), you have to mentally map the index to the value each time.

Performance and Memory Behavior

Both range and enumerate are lazy. range returns a range object that generates numbers on demand, and enumerate returns an iterator that wraps another iterator. Neither creates a full list of indices or pairs in memory.

However, there is a subtle performance difference when iterating over a sequence like a list. Using range and indexing requires an extra lookup operation each iteration: list[i]. This is O(1) for lists, but it still involves a __getitem__ call. enumerate retrieves the value directly from the iterator, which for a list is also O(1) but avoids the explicit indexing step. In practice, the difference is negligible for small collections, but for very large lists, enumerate can be slightly faster because it avoids the extra indexing operation.

For non-sequence iterables, the difference is more pronounced. If you try to use range on a generator, you will get a TypeError because generators do not support indexing. enumerate works seamlessly because it only needs to call next() on the underlying iterator.

The memory footprint is similar: both are O(1) in terms of additional memory beyond the iterable itself. range stores the start, stop, and step values, while enumerate stores the counter and the underlying iterator.

Common Mistakes and Edge Cases

One common mistake is using range(len(...)) on an object that is not indexable. For example, a set or a generator will raise a TypeError:

# This fails for i in range(len({1, 2, 3})): print(i)

Sets are not sequences, so len() works but indexing does not. The correct approach is to use enumerate:

for i, value in enumerate({1, 2, 3}): print(i, value)

Another edge case is when you need to iterate over a dictionary. range is useless here because dictionaries are not indexable by integer position. enumerate gives you the keys, and you can access values separately:

d = {"a": 1, "b": 2} for i, key in enumerate(d): print(i, key, d[key])

A subtle mistake is assuming that enumerate creates a list of tuples. It does not; it is an iterator. If you need a list, you must explicitly convert it:

pairs = list(enumerate(fruits))

This is useful when you need to store the index-value pairs for later use.

Choosing Between enumerate and range

The decision comes down to what you need inside the loop. Use enumerate when you need both the index and the value from a single iterable. This covers most iteration scenarios, especially when you are reading data and do not need to modify the collection.

Use range when you need to work with indices for other purposes:

  • Modifying the collection in place by index.
  • Accessing multiple collections by the same index (though zip is often better).
  • Skipping indices with a step.
  • Comparing adjacent elements.
  • Iterating over a slice of a list using range(len(...)) with slicing.

If you find yourself writing for i in range(len(iterable)) and then immediately accessing iterable[i], you should almost always use enumerate instead. It is more idiomatic, more readable, and avoids the extra indexing step.

For cases where you need both the index and the value from multiple iterables, consider combining enumerate with zip:

for i, (a, b) in enumerate(zip(list_a, list_b)): print(i, a, b)

This gives you the index and the paired values in one loop.

The choice is not about performance in most real-world code; it is about clarity and correctness. enumerate reduces the chance of indexing errors and works with any iterable. range is the right tool when the index itself is the focus, not the value it points to.

python enumerate vs range: Which to Use | RYUSLOG DEV