enumerate() for Index
Concepts covered: Indexed Iteration
A for loop over a list hands you each item and says nothing about where that item sat. Data work keeps asking for the position: a validation step has to name the row that failed, a cleanup has to write a corrected value back into the right slot, and a progress line has to say how far a batch has got. The usual first attempt is a counter bumped by hand, and it drifts the moment the body skips ahead.
amounts = [120, -5, 80, -2, 40]
row = 0
for amount in amounts:
if amount >= 0:
continue
print("row", row, "is negative:", amount)
row += 1Output:
row 0 is negative: -5 row 1 is negative: -2
The negative amounts sit at positions 1 and 3, and the report says 0 and 1. The continue sends control back to the top of the loop before row += 1 runs, so the counter counts only the steps that reached the bottom of the body. Moving the increment higher would fix this copy until someone adds another continue above it. The position should come from the loop itself, which is what enumerate() provides.
fruits = ["apple", "banana", "cherry"]
print("Manual approach:")
i = 0
for fruit in fruits:
print(str(i) + ": " + fruit)
i += 1
print()
print("With enumerate:")
for index, fruit in enumerate(fruits):
print(str(index) + ": " + fruit)Manual approach: 0: apple 1: banana 2: cherry With enumerate: 0: apple 1: banana 2: cherry
enumerate(amounts) wraps the list, and each step of the loop receives a pair: the position first, the item second. The header for i, amount in enumerate(amounts): splits each pair into 2 names with the same multiple assignment as a, b = 1, 2. Python moves the position forward on every step whatever the body does, so a continue can no longer knock it out of line with the list.
amounts = [120, -5, 80, -2, 40]
for i, amount in enumerate(amounts):
if amount >= 0:
continue
print("row", i, "is negative:", amount)Output:
row 1 is negative: -5 row 3 is negative: -2
| Step | i | amount | Reaches print? |
|---|---|---|---|
| 1 | 0 | 120 | no |
| 2 | 1 | -5 | yes |
| 3 | 2 | 80 | no |
| 4 | 3 | -2 | yes |
| 5 | 4 | 40 | no |
Choosing where the count starts
Positions start at 0 because list indexes do. A report that people read counts from 1, so enumerate takes a start argument that sets the first number. It renumbers the positions and moves nothing: the first item handed out is still the one stored at index 0.
files = ["orders.csv", "refunds.csv", "users.csv"]
for n, name in enumerate(files, start=1):
print("loading file", n, "of 3:", name)Output:
loading file 1 of 3: orders.csv loading file 2 of 3: refunds.csv loading file 3 of 3: users.csv
Writing back through the position
Position matters most when the loop changes the list. Assigning to the loop variable, amount = 0, points that name at a new value and leaves the list as it was. Assigning through the index, amounts[i] = 0, replaces the value stored in the list. The program below tries both.
amounts = [120, -5, 80, -2, 40]
for i, amount in enumerate(amounts):
if amount < 0:
amount = 0
print(amounts)
for i, amount in enumerate(amounts):
if amount < 0:
amounts[i] = 0
print(amounts)Output:
[120, -5, 80, -2, 40] [120, 0, 80, 0, 40]
This loop is meant to replace the 20 with 99. What does it print?
prices = [10, 20, 30]
for i, price in enumerate(prices, start=1):
if price == 20:
prices[i] = 99
print(prices)- [10, 99, 30]
- [10, 20, 99]
- [99, 20, 30]
- [10, 20, 30]
So start=1 belongs where the counter is only shown to a person, in a printed line or a report row. Any read or write into the list keeps using the default 0-based counter, which matches the indexes the list actually has.
About this lesson
This section is part of the Loops: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.