To iterate with an index in Python, use enumerate(): for i, x in enumerate(items): .... To iterate over several iterables in parallel, use zip(): for a, b in zip(list_a, list_b): .... To do both at once, combine them: for i, (a, b) in enumerate(zip(list_a, list_b)): .... On Python 3.10+, add strict=True to zip() if the inputs should always be the same length; it catches off-by-one bugs at runtime instead of silently truncating. The range(len(items)) pattern is officially an anti-pattern; this article retires it for good. This piece is one of 17 short explainers in our Python Concepts Explained reference.
Key Takeaways
enumerate(items) pairs each item with a counter, defaulting to zero. Pass start=1 for one-based indexing in user-facing output.
zip(a, b, c, ...) walks N iterables in parallel and yields tuples. It stops at the shortest input by default.
enumerate(zip(a, b)) gives you the index AND parallel items at once. The unpacking pattern is for i, (a_val, b_val) in ....
range(len(items)) is an anti-pattern. Every place it appears, enumerate or zip reads better and runs at least as fast.
Python 3.10+ adds strict=True to zip() (PEP 618). Use it whenever the iterables should logically have the same length; it turns silent truncation into a loud ValueError.
The three patterns build on each other: enumerate adds an index, zip stitches lists in parallel, the combination does both.
enumerate — Index With Every Value
enumerate() wraps any iterable and yields (index, value) tuples:
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
# 0: apple
# 1: banana
# 2: cherry
Pass start when you want a different starting count (one-based output for humans, line numbers offset from a known position, and so on):
for line_no, line in enumerate(open("config.ini"), start=1):
print(f"line {line_no}: {line.rstrip()}")
The (index, value) tuple gets unpacked into the two loop variables. Forget the unpacking and you get the raw tuple instead, which is the single most common gotcha (covered below).
zip — Parallel Iteration Across N Iterables
zip() walks several iterables side by side and yields tuples of corresponding items:
names = ["Alice", "Bob", "Charlie"]
ages = [30, 25, 35]
for name, age in zip(names, ages):
print(f"{name} is {age}")
# Alice is 30
# Bob is 25
# Charlie is 35
It accepts any number of inputs, not just two:
for name, age, city in zip(names, ages, ["NYC", "LA", "Chicago"]):
print(f"{name} ({age}) lives in {city}")
The result is an iterator, not a list. Calling list(zip(a, b)) materializes it if you need that. Like every well-behaved iterator, it's single-pass: once exhausted, you can't loop over it again.
enumerate(zip(...)) — When You Need Both at Once
The combination most articles treat as an afterthought is actually a workhorse:
The pattern reads cleanly once you recognize the shape: enumerate wraps the outer iteration and gives the index; zip produces the inner tuple. The parentheses around (name, score) tell Python to unpack the inner tuple, otherwise you'd bind the whole tuple to one name. Use this pattern any time you need a counter alongside two or more parallel sequences.
The range(len()) Anti-Pattern
Anywhere a Python tutorial uses for i in range(len(items)), there's a cleaner replacement. The table below covers the five common shapes:
Goal
Don't write
Write instead
Index + value from one list
for i in range(len(items)): x = items[i]
for i, x in enumerate(items)
Two lists in parallel
for i in range(len(A)): a, b = A[i], B[i]
for a, b in zip(A, B)
Index + two lists
for i in range(len(A)): a, b = A[i], B[i]
for i, (a, b) in enumerate(zip(A, B))
Catch length mismatches
manual len() check + assert
zip(A, B, strict=True) (3.10+)
Pad mismatched lengths
max(len(A), len(B)) + manual fill
itertools.zip_longest(A, B, fillvalue=...)
The "write instead" column is shorter, faster to read, and runs at least as fast in CPython. The "don't write" column also breaks on iterables that aren't sequences (generators, dict views, files), while the modern forms handle every iterable.
strict=True in Python 3.10+ — Catching Length Bugs
Python 3.10 added the strict keyword to zip() via PEP 618. Without it, zip() silently stops at the shortest input, which silently hides bugs:
# Silent truncation: Charlie is lost
names = ["Alice", "Bob", "Charlie"]
ages = [30, 25]
for name, age in zip(names, ages):
print(f"{name}: {age}")
# Alice: 30
# Bob: 25
# (Charlie silently dropped)
# With strict=True: explicit failure
for name, age in zip(names, ages, strict=True):
print(f"{name}: {age}")
# Alice: 30
# Bob: 25
# ValueError: zip() argument 2 is shorter than argument 1
Use strict=True whenever the iterables are supposed to have the same length. The runtime check costs almost nothing and turns a silent data-loss bug into a loud failure you'll catch immediately. The PEP author summarized the motivation as "explicit is better than implicit, and silent corruption is worse than a noisy crash."
zip_longest for Padded Iteration
When the iterables are intentionally different lengths and you want to iterate over the full length of the longest, use itertools.zip_longest:
from itertools import zip_longest
names = ["Alice", "Bob", "Charlie", "Diana"]
ages = [30, 25]
for name, age in zip_longest(names, ages, fillvalue="(unknown)"):
print(f"{name}: {age}")
# Alice: 30
# Bob: 25
# Charlie: (unknown)
# Diana: (unknown)
The default fillvalue is None. Pass any sentinel that fits your data. The choice between zip(strict=True) and zip_longest is intent: should mismatches fail, or should they pad?
dict.items() Is Just zip in Disguise
A connection nobody else makes: iterating a dictionary with dict.items() is essentially the same as iterating zip(keys, values). The unpacking pattern is identical:
person = {"name": "Alice", "age": 30, "city": "NYC"}
# dict.items() unpacks like zip
for key, value in person.items():
print(f"{key} = {value}")
# zip-on-separate-lists gives the same shape
for key, value in zip(person.keys(), person.values()):
print(f"{key} = {value}")
Combine with enumerate when you also need a counter over a dict:
for i, (key, value) in enumerate(person.items(), start=1):
print(f"{i}. {key} = {value}")
Same unpacking shape as enumerate(zip(A, B)) from earlier. Once the pattern clicks, it transfers everywhere.
Common Mistakes
Four traps to watch for:Mistake 1: forgetting tuple unpacking.for item in enumerate(items) binds the whole (i, x) tuple to item. You'll then have to write item[0] and item[1], which is the worst of both worlds. Always unpack: for i, x in enumerate(items).Mistake 2: relying on silent truncation when strict=True would help. Two lists that drift out of sync (off-by-one bug, missing data, async fetch dropping a row) will produce a confidently wrong loop. Default to strict=True whenever the iterables should match.Mistake 3: iterating a zip() result twice. The result is an iterator, exhausted after one pass. Wrap in list(...) if you genuinely need it twice, or call zip(...) again to get a fresh iterator.Mistake 4: reaching for range(len(items)) out of habit. If you came to Python from C, Java, or JavaScript, this one fires automatically. Every time you type range(len(, pause and check: is it enumerate? zip? enumerate(zip)? It almost always is.
Frequently Asked Questions
How do I iterate with an index in Python?
Use the built-in enumerate() function: for i, x in enumerate(items): .... It pairs each item with a counter starting at zero. Pass start=1 (or any integer) to begin counting from there. Avoid for i in range(len(items)): items[i]; it's the classic Python anti-pattern, slower to read and harder to maintain.
How do I iterate over two lists in parallel in Python?
Use zip(): for a, b in zip(list_a, list_b): .... It produces tuples by matching items at the same position in each iterable. zip() stops at the shortest input by default; on Python 3.10+, pass strict=True to raise a ValueError if the inputs differ in length. For three or more lists, zip(a, b, c) just keeps going.
How do I get the index AND iterate two lists at once?
Combine the two: for i, (a, b) in enumerate(zip(list_a, list_b)): .... The inner zip pairs the lists; the outer enumerate adds the index. Note the parentheses around (a, b); they tell Python to unpack the inner tuple. The pattern reads cleanly and beats every range(len(...)) variant.
What happens when zip's lists are different lengths?
By default, zip() silently stops at the shortest input, which can hide bugs. Python 3.10 added strict=True (PEP 618), which raises ValueError when the lengths don't match. For deliberate padding instead of trimming, use itertools.zip_longest(), which fills missing positions with None (or a custom fillvalue).
The Bottom Line: One Toolkit, Four Patterns
Indexed and parallel iteration in Python comes down to four shapes: enumerate(x) for index, zip(a, b) for parallel, enumerate(zip(a, b)) for both, and strict=True when length should be guaranteed. Retire range(len()) everywhere it shows up; the modern forms are shorter, safer, and work on every iterable, not just sequences. Once the unpacking shape clicks, dict.items() stops feeling like a special case and starts feeling like the zip pattern it's always been. For the rest of the most-asked Python concept questions, browse the full Python Concepts Explained index.
Drill the Loop Patterns Until They're Reflex
CodeGym's Python track turns iteration idioms into muscle memory through 800+ hands-on tasks across 62 levels. The AI validator checks every submission in seconds; the AI mentor explains what broke when you get stuck. First level free; full plan on the pricing page.
Start learning Python (free) →
GO TO FULL VERSION