How to loop with an index in Python
Wrap the iterable in enumerate() and unpack two names: for i, item in enumerate(items). You get the index and the item, with no manual counter and no indexing back into the list.
Python's for loop iterates over values, not positions — which is why the C-style for i in range(len(items)) feels like it's missing something. It is: enumerate() is the built-in that hands you the counter alongside the value. Each example below runs on this page — hit Run, then edit and run it again.
1enumerate()Recommended
enumerate() yields (index, item) pairs, and the loop target unpacks them into two names. The index starts at 0, matching normal list positions.
Output
Output: 0 Python, 1 Go, 2 Rust. Nothing is indexed back into the list, so the loop can't drift out of sync with it — and it works on any iterable, not just sequences.
2Start counting at 1
enumerate() takes a second argument, start, which sets the first value of the counter. Use start=1 for anything a human reads — numbered lists, report rows, line numbers — instead of writing index + 1 everywhere.
Output
Prints 1. Write tests through 3. Ship it. start only shifts the counter — it does not skip items, so the whole iterable is still visited.
3range(len(items)) — the C-style habit
This is the loop people carry over from C or Java. It works on a list, but it's strictly worse: you pay an extra items[i] lookup on every line that needs the value, the bounds are yours to get right (len(items) - 1 and + 1 are the classic off-by-one bugs), and it only works on things with a length. Generators, files, zip() objects and map() objects have no len() at all.
Output
The first loop prints the same three lines as enumerate() did; the second prints 0 0, 1 1, 2 4, 3 9. Swap enumerate for range(len(squares)) and you get TypeError: object of type 'generator' has no len(). Reach for range(len(...)) only when you genuinely need positions without the values — say, mutating items[i] in place.
4Two lists at once with zip()
When the "index" you actually wanted was just a way to line up two lists, use zip() — it walks them in lockstep and yields tuples. Need the position as well? Wrap the zip() in enumerate() and unpack the inner tuple in parentheses.
Output
zip() stops at the shortest input and drops the rest silently — with [1, 2, 3] and [1, 2] you get two pairs, no warning. Pass strict=True to raise ValueError: zip() argument 2 is shorter than argument 1 instead.
5Which should you use?
| Method | Readability | Works on any iterable | Best for |
|---|---|---|---|
| enumerate(items) | Great | Yes | Index + item, almost always |
| enumerate(items, start=1) | Great | Yes | Human-facing numbering |
| zip(a, b) | Great | Yes | Two lists in lockstep |
| enumerate(zip(a, b)) | Good | Yes | Index + both values |
| range(len(items)) | Verbose | No — needs len() | Positions without values |
6Common variation: dicts and filtering
A dict iterates over its keys, so pass d.items() to enumerate() and unpack the pair in parentheses to get counter, key and value at once — insertion order is guaranteed, so the numbering is stable. The same trick keeps the original index when you filter: enumerate() first, then the if, so the surviving items still carry the position they had in the source list.
Output
The dict loop prints 1 host localhost, 2 port 9090, 3 debug True, and the comprehension prints [(0, 12), (2, 30), (4, 22)] — note the gaps at 1 and 3, which is exactly the point: filtering first would have renumbered them 0, 1, 2 and lost where they came from.
Frequently asked questions
What is the Pythonic way to get the index in a for loop?
for i, item in enumerate(items). enumerate() is a built-in that yields (index, item) tuples, so you get the counter and the value together without a manual variable or an items[i] lookup.
How do I make enumerate start at 1 instead of 0?
Pass the start argument: for n, item in enumerate(items, start=1). It only shifts the counter — no items are skipped — which makes it the clean way to print numbered lists without writing i + 1.
Why is for i in range(len(items)) considered bad practice?
It needs an extra items[i] lookup for every use of the value, it puts the loop bounds (and the off-by-one bugs) on you, and it only works on objects that have a len(). Generators, files, zip() and map() objects raise TypeError: object of type generator has no len(). Use it only when you want positions without the values, such as assigning to items[i].
How do I loop over two lists at the same time with an index?
Use for i, (a, b) in enumerate(zip(list_a, list_b)) — zip() pairs the values and enumerate() adds the counter. Note that zip() stops at the shortest list; pass strict=True to raise a ValueError on a length mismatch instead of silently truncating.