How to access the index in a Python for loop
Python’s for loop hands you the elements, not their positions. When you need
the position as well there is a built-in for it, and a C-style workaround that
most people write first.
The answer
for i, value in enumerate(items):
...
0 a
1 b
2 c
enumerate
yields (index, value) pairs. It is lazy, so it builds nothing.
Counting from one is a parameter, not arithmetic:
enumerate(items, start=1) : [(1, 'a'), (2, 'b'), (3, 'c')]
That start= is the bit people miss, and i + 1 scattered through a loop body
is the result.
The version most people write first
for i in range(len(items)):
value = items[i]
It works. It also indexes back into the list on every iteration, which is both
slower and noisier than letting enumerate hand you the value it already had.
The stronger argument is generality. range(len(x)) needs len(), and plenty
of things you loop over do not have one:
enumerate on a generator : [(0, 'x'), (1, 'y')]
len(a generator) -> TypeError: object of type 'generator' has no len()
enumerate works on any iterable — a generator, an open file, a dict, a
set, a database cursor. range(len()) works on sequences only.
Two sequences at once
If you reached for indices in order to walk two lists together, the answer is
zip, not indices:
zip(names, ages) : [('ada', 36), ('alan', 41)]
And if you want the position too, the two nest:
enumerate(zip(names, ages)) : [(0, ('ada', 36)), (1, ('alan', 41))]
One thing to know about zip: it stops at the shorter input, silently.
zip stops at the shorter one : [(1, 'a'), (2, 'b')]
Since Python 3.10 you can ask it not to, via PEP 618:
zip(..., strict=True) -> ValueError: zip() argument 2 is shorter than argument 1
If the two sequences are supposed to be the same length, say so. Silent truncation is a bug that produces plausible output.
Do not modify the list while you iterate
This is the reason people go looking for the index in the first place, and it is worth showing what happens:
nums = [1, 2, 3, 4, 5, 6]
for i, v in enumerate(nums):
if v % 2 == 0:
nums.remove(v)
removing while iterating : visited [1, 2, 4, 6], list is now [1, 3, 5]
The result happens to look right, and the process was not. The loop visited
1, 2, 4, 6 — it never saw 3 or 5. Each removal shifts everything left
while the iterator’s position keeps moving right, so one element is skipped
each time.
Here the skipped elements were odd, so the output survived. Change the condition and it will not.
Build a new list instead:
nums = [v for v in nums if v % 2]
build a new list instead : [1, 3, 5]
Assigning through the index is fine, since the length does not change:
for i, v in enumerate(letters):
letters[i] = v.upper()
assigning through the index works fine : ['A', 'B', 'C']
A detail worth knowing
enumerate yields tuples, and you do not have to unpack them:
list(enumerate('ab')) : [(0, 'a'), (1, 'b')]
without unpacking : (0, 'a')
Which is occasionally useful — sorted(enumerate(x), key=...) sorts values
while keeping their original positions attached.
That is also the difference between these two:
list(enumerate(reversed(items))) : [(0, 'c'), (1, 'b'), (2, 'a')]
reversed(list(enumerate(items))) : [(2, 'c'), (1, 'b'), (0, 'a')]
The first renumbers from zero, the second keeps the original indices and walks them backwards. Almost always the second is what you meant.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).