Skip to Content
For Loops

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_2
  • iterable – 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. x in for 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) # 8

Loop 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 typeWhat is iterated overExample
listEach elementfor x in [2, 4, 6]:x = 2, 4, 6
tupleEach elementfor x in (2, 4, 6):x = 2, 4, 6
strEach characterfor c in "24":c = "2", "4"
rangeEach integer in the rangefor i in range(2, 8, 2): → 2, 4, 6
setEach element (order varies)for x in {2, 4, 6}: → order not guaranteed
dictKeys onlyfor k in d: → each key
dict.keys()KeysSame as iterating the dict
dict.values()Valuesfor v in d.values(): → each value
dict.items()(key, value) pairsfor 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 6

Set 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.”

CallWhat you getExample
range(stop)0 up to but not including stoprange(6) → 0,1,2,3,4,5
range(start, stop)start up to but not including stoprange(2, 8) → 2,3,4,5,6,7
range(start, stop, step)start, start+step, … until you reach or pass stoprange(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, 2

When 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 8

Fewer 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 paired

In 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)) # ValueError

Two 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 12

Mismatch: 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 break

Handy 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-op

Nested 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 confusing

Safe approaches:

ApproachWhen to use it
Loop over a copyYou 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 endYou’re removing by index so earlier indices don’t shift
Collect what to remove, then remove in a second passYou 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 exhausted

Worth 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 sequenceYou 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.

Last updated on