Python For Loops: range(), enumerate() and zip() Explained
A Python for loop visits each item of an iterable. How range() counts, when to use enumerate and zip, looping over a dict and why not to edit a list mid-loop.
- Course: Python study plan
- Module: Control flow
- Kind: Lesson
- Reading time: 14 min
- Runtime: CPython 3.11
How does a for loop work in Python?
A Python for loop is a for-each: it takes any iterable — a list, string, range, dictionary, file or generator — and binds the loop variable to each element in turn. There is no index unless you ask for one with enumerate; range supplies counting and zip walks two sequences together. break, continue and else work as they do in a while loop.
Lesson
Python's for is a for-each: it takes an iterable — a list, a string, a range, a dictionary, a file, a generator — and binds the loop variable to each element in turn. There is no index unless you ask for one, and asking is done with enumerate; walking two sequences together is zip; counting is range. Learners from C write for i in range(len(xs)): x = xs[i], which works and marks them as learners. This lesson fixes the idioms, explains range precisely, and states the one rule about modifying a collection while iterating over it.
The statement
for ch in "abc":
print(ch)
for name in ["ada", "grace"]:
print(name.title())
for key in {"a": 1, "b": 2}: # a dict iterates its keys
print(key)
The loop variable is an ordinary name bound in the enclosing scope; it keeps its last value after the loop (for i in range(3): pass; print(i) prints 2), and rebinding it inside the body does not affect the iteration — the next value comes from the iterable, not from the variable. break, continue and else work exactly as in while: else runs when the iterable is exhausted without a break.
for x in xs:
if x < 0:
print("has a negative")
break
else:
print("all non-negative")
range
range(stop), range(start, stop), range(start, stop, step) — half-open, like slicing: start included, stop excluded.
list(range(5)) # [0, 1, 2, 3, 4]
list(range(2, 5)) # [2, 3, 4]
list(range(0, 10, 3)) # [0, 3, 6, 9]
list(range(5, 0, -1)) # [5, 4, 3, 2, 1]
list(range(3, 3)) # []
Half-open ranges compose: range(0, n) then range(n, m) cover 0 … m-1 exactly once; the count of elements is stop - start for step 1; range(len(xs)) is exactly the valid indices. A range is not a list — it is a small object that computes elements on demand, so range(10 ** 9) costs nothing until iterated, and 5 in range(0, 100, 5) is answered arithmetically. Counting down is range(n - 1, -1, -1), or reversed(range(n)), which reads better.
The loop for _ in range(n): is the idiom for "n times, the number does not matter"; the underscore is a convention meaning "unused".
enumerate: index and element
for i, name in enumerate(names): # 0-based
print(i, name)
for i, name in enumerate(names, start=1): # 1-based, for display
print(f"{i}. {name}")
enumerate yields (index, element) pairs, unpacked into two names by the for. Reach for it whenever the body needs the position — to print a line number, to compare with a neighbour xs[i + 1], to record where a match was found. for i in range(len(xs)) is the older spelling and is right only when the body needs the index and not the element, or must write xs[i] = ….
zip: parallel sequences
for name, score in zip(names, scores):
print(f"{name}: {score}")
for i, (name, score) in enumerate(zip(names, scores), start=1):
...
zip stops at the shortest input, silently; zip(a, b, strict=True) (3.10) raises ValueError on a length mismatch, which is what you want when the sequences are supposed to correspond. dict(zip(keys, values)) builds a mapping from two lists; zip(*grid) transposes a list of rows into columns (Module 6). itertools.zip_longest pads the shorter one instead of truncating.
Iterating dictionaries
for k in d gives keys; for v in d.values() values; for k, v in d.items() both. Insertion order is preserved (since 3.7), so a dictionary iterates in the order its keys were added. for k in sorted(d) iterates the keys in sorted order.
Nested loops
for r in range(rows):
for c in range(cols):
visit(r, c)
Leaving both loops at once has no break 2. The options: put the loops in a function and return; set a flag and test it in the outer loop; or iterate the pairs with itertools.product(range(rows), range(cols)), which makes it one loop with one break. The function is the cleanest when the search is a unit of work.
Modifying while iterating
Do not add to or remove from a list, dict or set while a for loop is iterating over it. Removing shifts the elements and the loop skips one; adding can make it infinite; a dict raises RuntimeError: dictionary changed size during iteration. The fixes are to iterate over a copy (for x in list(xs):), to build a new collection (kept = [x for x in xs if keep(x)] — the comprehension is almost always what you meant), or to collect the changes and apply them after the loop. Modifying elements in place (xs[i] = …) is fine.
Pitfalls
for i in range(len(xs)): x = xs[i]wherefor x in xsorenumeratewas meant.range(1, n)whennshould be included: the stop is excluded.ziptruncating silently when the lists should have matched —strict=True.xs.remove(x)insidefor x in xs.- Expecting a
forloop to have its own scope; the loop variable and everything assigned in the body are visible after it. for k, v in d:— iterating a dict gives keys;.items()gives pairs.
Key takeaways
foriterates the elements of any iterable;break,continue,elseas withwhile.range(start, stop, step)is half-open and lazy;range(len(xs))is the valid indices;for _ in range(n)repeats.enumeratefor index and element,zipfor parallel sequences (strict=Trueto check lengths),.items()for dict pairs.- Leave nested loops with a function
returnoritertools.product. - Never change a collection's size while iterating it; iterate a copy or build a new one.
Common questions
How does range() work in Python?
range(start, stop, step) produces integers from start up to but not including stop: range(5) gives 0 to 4 and range(0, 10, 3) gives 0, 3, 6, 9. It is lazy, computing values on demand, so range(10 ** 9) costs nothing until iterated. Count down with range(n - 1, -1, -1) or reversed(range(n)).
How do I get the index in a Python for loop?
Use enumerate: for i, name in enumerate(names): yields each index with its element, and enumerate(names, start=1) counts from 1 for display. for i in range(len(xs)) is right only when the body needs the index and not the element, or assigns to xs[i].
What does zip do in Python?
zip walks several iterables in parallel, yielding tuples of corresponding elements, as in for name, score in zip(names, scores):. It stops silently at the shortest input; since Python 3.10, zip(a, b, strict=True) raises ValueError on a length mismatch. dict(zip(keys, values)) builds a mapping from two lists.
How do I loop over a dictionary in Python?
for k in d iterates the keys, for v in d.values() the values, and for k, v in d.items() both at once. Dictionaries keep insertion order since Python 3.7, so they iterate in the order their keys were added; for k in sorted(d) visits the keys in sorted order.
How do I remove items from a list while iterating over it in Python?
Do not remove them inside a loop over that same list: the elements shift and the loop skips one. Build a new list instead, such as kept = [x for x in xs if keep(x)], or iterate over a copy with for x in list(xs):. Changing a dictionary's size during iteration raises RuntimeError.
Exercises
Multiplication table
Print the n × n multiplication table with nested for loops over range(1, n + 1). Every entry is right-aligned in a field as wide as the largest product, and entries on a row are separated by one space.
Input: n (1–12). Output: n rows.
4
prints
1 2 3 4
2 4 6 8
3 6 9 12
4 8 12 16Ranked pairs
Two lines hold names and integer scores in matching order. Print them numbered from 1 using enumerate over zip(names, scores, strict=True), then the name with the highest score (the first one on a tie). If the two lines have different lengths, strict=True raises ValueError — catch it and print length mismatch instead.
Input: a line of names, then a line of integers. Output: <i>. <name> <score> per pair, then best: <name>; or the single line length mismatch.
ada grace linus
90 95 88
prints
1. ada 90
2. grace 95
3. linus 88
best: graceIn this module: Control flow
← While loops — repeat until a condition changes · Match statements — structural pattern matching →