For Loops
A for loop goes over an iterable (list, string, range, whatever) one item at a time and assigns each to a variable. Run the body once per item; when there’s nothing left, the loop ends. If you’ve got a sequence or you know how many times to run, for is what you want. Save while for when you’re not sure how many iterations you’ll need.
enumerate(),zip(), and nested loops show up all the time. Two things that bite people: the loop variable still has its last value after the loop, and changing a list while you’re iterating over it.
Syntax and Structure
General form:
for target in iterable:
# loop body - runs once per element
statement_1
statement_2iterable– Anything you can loop over: list, tuple, string, range, set, dict keys/values/items, file lines. One item per turn until it’s done.target– The variable that gets each item (e.g.xinfor x in items). Gets reassigned every time. After the loop it still holds the last value, which catches some people off guard.- Loop body – The indented block under
for. Runs once per item.
Example:
vals = [2, 4, 6, 8]
for x in vals:
print(x)
# Prints: 2, 4, 6, 8
# After the loop, x still exists and holds the last element
print(x) # 8Loop variable scope: for doesn’t create a new scope. The loop variable lives in the enclosing scope (function or module) and keeps the last value after the loop. Coming from other languages, it’s easy to assume the variable is “gone” after the loop; it isn’t.
Iterating Over Common Types
What you get on each pass depends on the type of thing you’re looping over.
| Iterable type | What is iterated over | Example |
|---|---|---|
| list | Each element | for x in [2, 4, 6]: → x = 2, 4, 6 |
| tuple | Each element | for x in (2, 4, 6): → x = 2, 4, 6 |
| str | Each character | for c in "24": → c = "2", "4" |
| range | Each integer in the range | for i in range(2, 8, 2): → 2, 4, 6 |
| set | Each element (order varies) | for x in {2, 4, 6}: → order not guaranteed |
| dict | Keys only | for k in d: → each key |
dict.keys() | Keys | Same as iterating the dict |
dict.values() | Values | for v in d.values(): → each value |
dict.items() | (key, value) pairs | for k, v in d.items(): → unpacked pairs |
Example:
# List and tuple - elements in order
for x in [2, 4, 6, 8]:
print(x) # 2, 4, 6, 8
# String - characters
for c in "24":
print(c) # "2", "4"
# Dict - keys by default
data = {"a": 2, "b": 4, "c": 6}
for k in data:
print(k, data[k]) # a 2, b 4, c 6
# Dict - keys and values with items()
for k, v in data.items():
print(k, v) # a 2, b 4, c 6Set iteration: A set yields each element once, but the order is arbitrary and can change between runs. Don’t rely on set order in a loop.
range() in For Loops
range gives you integers in a sequence without actually building a list. Handy when you need indices or just “do this N times.”
| Call | What you get | Example |
|---|---|---|
range(stop) | 0 up to but not including stop | range(6) → 0,1,2,3,4,5 |
range(start, stop) | start up to but not including stop | range(2, 8) → 2,3,4,5,6,7 |
range(start, stop, step) | start, start+step, … until you reach or pass stop | range(2, 10, 2) → 2,4,6,8 |
Important: stop is never included. range(2, 8) gives 2 through 7, not 8. Use a negative step to count down; then start should be bigger than stop.
Example (even digits only in sample data):
# 0, 2, 4, 6 - stop 8 is exclusive
for i in range(0, 8, 2):
print(i) # 0, 2, 4, 6
# Index-based iteration over a sequence
vals = [2, 4, 6, 8, 10]
for i in range(len(vals)):
print(i, vals[i]) # 0 2, 1 4, 2 6, 3 8, 4 10
# Countdown with negative step
for i in range(8, 0, -2):
print(i) # 8, 6, 4, 2When to use range(len(…)): Only when you really need the index (e.g. to change an item in place or line up two lists by position). If you just need the items, for x in seq is enough. Need both? enumerate() is cleaner than messing with indices yourself.
enumerate()
enumerate(iterable, start=0) gives you (index, item) pairs so you don’t have to mess with range(len(...)) or a manual counter.
vals = [2, 4, 6, 8]
for i, x in enumerate(vals):
print(i, x) # 0 2, 1 4, 2 6, 3 8
# Start counting from 2
for i, x in enumerate(vals, start=2):
print(i, x) # 2 2, 3 4, 4 6, 5 8Fewer off-by-one mistakes, and the intent is obvious. Use it whenever you need the index and the item together.
zip()
zip() lines up two or more iterables: first with first, second with second, and so on. Call it as zip(a, b, c) with separate arguments (not one list). Stops when the shortest one runs out.
Example:
keys = ["a", "b", "c"]
vals = [2, 4, 6]
for k, v in zip(keys, vals):
print(k, v) # a 2, b 4, c 6
# Three (or more) iterables
labels = ["x", "y", "z"]
nums = [2, 4, 6]
flags = [True, False, True]
for label, n, flag in zip(labels, nums, flags):
print(label, n, flag)If one list is longer, the extras never get paired. No error, it just stops.
a = [2, 4, 6, 8, 10]
b = [20, 40]
for x, y in zip(a, b):
print(x, y) # (2, 20), (4, 40) - 6, 8, 10 never pairedIn Python 3.10+ you can pass strict=True and get a ValueError if the lengths don’t match instead of quietly dropping the tail.
# Python 3.10+
list(zip([2, 4], [6, 8], strict=True)) # [(2, 6), (4, 8)]
# list(zip([2, 4, 6], [8, 10], strict=True)) # ValueErrorTwo parallel lists into a dict: dict(zip(keys, values)). Does exactly what it sounds like.
keys = ["a", "b", "c"]
vals = [2, 4, 6]
mapping = dict(zip(keys, vals)) # {"a": 2, "b": 4, "c": 6}Unpacking in the Loop Target
The loop variable can be an unpacking target. You see that a lot with dict.items(), enumerate(), and zip().
Example:
pairs = [(2, 4), (6, 8), (10, 12)]
for a, b in pairs:
print(a + b) # 6, 14, 22
# Nested structure
data = [(2, (4, 6)), (8, (10, 12))]
for x, (y, z) in data:
print(x, y, z) # 2 4 6, 8 10 12Mismatch: Wrong number of variables (e.g. for a, b in [(2, 4, 6)]) and you get ValueError. Three values, two names. Match them or use *rest to soak up the rest.
The else Clause on for
for loops can have an else. The else block runs only when the loop runs to completion without hitting break. Hit break and the else is skipped.
Lots of people assume else means “when the list is empty.” It doesn’t. It means “when we didn’t break.”
Example:
# Search: else runs when no match found
vals = [2, 4, 6, 8]
for x in vals:
if x == 6:
print("found")
break
else:
print("not found") # skipped - break was hit
# No break - else runs
for x in vals:
if x == 10:
break
else:
print("not found") # printed - loop finished without breakHandy in search loops: put your “not found” handling in else and you don’t need a separate flag.
break, continue, and pass
- break - Stops the loop immediately. No more iterations; any else on the loop is skipped.
- continue - Skips the rest of this round and jumps to the next item. The loop keeps going.
- pass - Does nothing. Use when you need a placeholder (e.g. an empty loop body).
Example:
# break - stop at first even multiple of 4
for x in [2, 4, 6, 8, 10, 12]:
if x % 4 == 0:
print(x)
break # 4 printed; 8, 12 never reached
# continue - skip zeros (conceptually; using evens)
for x in [2, 4, 0, 6, 8]:
if x == 0:
continue
print(x) # 2, 4, 6, 8 - 0 skipped
# pass - stub for future logic
for x in [2, 4, 6]:
pass # no-opNested loops: break only exits the innermost loop. To get out of several levels, use a flag, return from a function, or an exception.
Modifying a Collection While Iterating
Change a list while you’re looping over it and you can skip elements, hit the same one twice, or trigger an error. Safe options: loop over a copy (e.g. list(vals)), or build a new list and assign it back.
Why it’s tricky:
# Unreliable: removing while iterating can skip elements
vals = [2, 4, 6, 4, 8]
for x in vals:
if x == 4:
vals.remove(x) # first 4 removed; the next 4 might be skipped
# Result can be wrong or confusingSafe approaches:
| Approach | When to use it |
|---|---|
| Loop over a copy | You need to remove or change items: for x in list(vals): |
| Build a new list (e.g. list comprehension) | You want a filtered or transformed list |
| Loop by index from the end | You’re removing by index so earlier indices don’t shift |
| Collect what to remove, then remove in a second pass | You want to avoid changing the list while iterating |
Example (iterate over copy):
vals = [2, 4, 6, 4, 8]
for x in list(vals):
if x == 4:
vals.remove(x)
# vals = [2, 6, 8]Reverse index loop: When removing by index, go from the end so indices don’t shift.
vals = [2, 4, 6, 8, 10]
for i in range(len(vals) - 1, -1, -1):
if vals[i] % 4 == 0:
del vals[i]
# vals = [2, 6, 10]Dictionaries: Don’t add or remove keys while iterating - you can get RuntimeError. Loop over list(d) or a copy of items(), or build a new dict.
Nested For Loops
Put one for inside another and the inner loop runs to completion for every single step of the outer. So you get (outer length × inner length) iterations total.
Example:
rows = [2, 4]
cols = [6, 8, 10]
for r in rows:
for c in cols:
print(r, c)
# (2,6), (2,8), (2,10), (4,6), (4,8), (4,10)Good for things like rows × columns, or “every combo of A with every B.” Also when you’ve got lists of lists and need to walk the inner ones. If you literally want every pair from two iterables, itertools.product is usually nicer than writing the nests yourself.
Iterables vs Iterators
The thing after in in a for loop is an iterable. Python turns it into an iterator behind the scenes; that’s what actually hands you one value at a time.
Lists, tuples, strings, range, dicts, sets: those are iterables. You can loop over them again and again. An iterator is the object that does the work of giving you the next value. The catch: once you’ve gone through it once, it’s spent. Loop over the same iterator again and you get nothing.
So a list is reusable. A generator or something like map/filter gives you an iterator that’s one-shot. Need to loop twice? Make a new iterator or turn it into a list.
Example:
r = range(2, 8, 2)
list(r) # [2, 4, 6]
list(r) # [2, 4, 6] - range is not an iterator; it is reusable
it = iter([2, 4, 6])
list(it) # [2, 4, 6]
list(it) # [] - iterator exhaustedWorth keeping in mind if you reuse the same object in two loops or pass an iterator into something that eats it.
When to Use for vs while
| Use for when … | Use while when … |
|---|---|
| You have a list, range, or other sequence | You don’t know how many times you’ll loop |
| You want each item (or index with enumerate) | You’re waiting for a condition (e.g. user types “quit”) |
Got a list or a known number of runs? for. “Keep going until something happens”? while. When both could work, for with range(N) is usually the safer bet.
Tricky Behaviors
Loop variable persists after the loop
The loop variable keeps the last value after the loop ends. It isn’t limited to the loop - it’s in the same scope as the rest of your code. Easy to forget if you reuse the name later.
dict in for - keys only
for x in d gives you keys only. For key and value: for k, v in d.items(). For values only: for v in d.values().
set - order not guaranteed
You get each item once, but the order can change. Don’t depend on it.
range() - stop is exclusive
range(2, 8) gives 2 through 7, not 8. The stop value is never included. Classic off-by-one if you forget.
zip() - shortest wins
zip(a, b) stops when the shorter list runs out. Extra items in the longer one are ignored. Use zip(..., strict=True) (Python 3.10+) to get an error if the lengths differ.
zip() - pass separate arguments
Call it as zip(a, b, c) - separate arguments, not one list. So zip(*iterables) means the parameter collects multiple iterables; you pass them like zip(list1, list2).
Unpacking - length must match
for a, b in [(2, 4), (6, 8)] works. for a, b in [(2, 4, 6)] raises ValueError - three values, two variables. Match the count or use *rest to collect the rest.
for-else - “no break,” not “empty”
else runs when the loop finishes without break. It doesn’t mean “the list was empty.” Often misunderstood.
break in nested loops - innermost only
break exits only the loop it’s in. The outer loop keeps going. To exit several levels: use a flag, return from a function, or an exception.
Modifying while iterating
Changing a list (add/remove) while you loop over it can skip or repeat items or raise an error. Loop over a copy (list(seq)) or build a new list. Same idea for dicts: don’t change keys while iterating; use a copy.
Iterator - single use
An iterator is used up after one full loop. A second loop over the same iterator does nothing. Lists and ranges are iterables you can loop over again; generators and map/filter iterators are one-shot.
Interview Questions
What does the loop variable hold after a for loop ends?
The last value. The variable stays in scope; it isn’t limited to the loop.
How do you iterate over a dictionary’s keys and values together?
for k, v in d.items(). For keys only use for k in d; for values only use for v in d.values().
Why not rely on set order in a for loop?
Sets don’t guarantee order; it can change. Use a set for uniqueness or fast “is it in here?” - not for a fixed order.
Is the stop value in range(start, stop) included?
No. range(2, 8) gives 2 through 7. The stop is always exclusive.
How do you loop over a sequence in reverse order?
for x in reversed(lst):. For index-based reverse (e.g. when modifying or removing by index): for i in range(len(lst)-1, -1, -1):.
Why prefer enumerate() over range(len(…))?
You get (index, item) pairs with no manual counter. Clearer and fewer off-by-one errors. Use range(len(...)) only when you need the index alone (e.g. to change an item by position).
Why does zip() stop at the shortest iterable?
It pairs first with first, second with second, and so on. When one list runs out, there’s nothing left to pair - so it stops. Use zip(..., strict=True) (Python 3.10+) if you want an error when lengths differ.
When would you use zip() with strict=True?
When the lists should be the same length and you want a ValueError instead of silently dropping extras. Python 3.10+.
How do you build a dictionary from two lists (keys and values)?
dict(zip(keys_list, values_list)). Stops at the shorter list.
What happens if the loop target has fewer or more names than the values?
ValueError. The count has to match. Use *rest to collect the rest: for first, *rest in items:.
When does the else clause on a for loop run?
When the loop finishes without break. If you break, else is skipped. So else = “no break.” Handy for “not found” in search loops.
What does break do in a nested for loop?
It exits only the innermost loop. Use a flag, return, or an exception to get out of more than one level.
How do you skip the rest of the current iteration and continue to the next?
continue. pass doesn’t skip - it does nothing and execution goes to the next line.
What happens if you modify a list while iterating over it?
You can skip items, see the same one twice, or get an error. Loop over a copy (for x in list(lst):) or build a new list and reassign.
How do you safely remove items from a list during iteration?
Loop over a copy: for x in list(lst): and remove inside the loop. Or build a new list with a comprehension. If removing by index, loop from the end so indices don’t shift.
When would you use nested for loops?
Whenever you need “each thing from here with each thing from there” (e.g. rows and columns, or all pairs). Same idea when you’re walking nested structures like lists of lists. For the “every pair” case, itertools.product often reads better than stacking loops.
What is the difference between an iterable and an iterator?
The iterable is what you put after in (list, range, etc.). You can loop over it as many times as you like. The iterator is what actually does the work of giving you the next value. Run through it once and it’s done; a second loop over the same iterator gives you nothing. So iter(list) gives you a one-shot iterator from a list.
When should you use a for loop instead of a while loop?
for when you have something to iterate (a sequence) or a fixed number of times. while when you’re waiting on a condition (user types “quit,” a flag changes, something like that). If you could do it either way, for tends to be easier to get right.