For Loops and range()
Do the same thing to every item, without writing it out every time. A for loop walks a collection; range() manufactures one to walk.
Overview
Iterating over items, not indices
Python’s for loop takes each element of a collection in turn:
for name in ["ana", "bo", "cy"]:
print(name)Note what is absent: no counter, no length, no indexing. The loop variable holds the element, not its position. Coming from C or Java the instinct is for i in range(len(items)) followed by items[i], and that is almost always the wrong shape in Python — it is longer, slower to read, and introduces an index you can get wrong.
When you genuinely need the position as well, enumerate gives you both:
for i, name in enumerate(items):And to walk two sequences together, zip pairs them by position:
for name, score in zip(names, scores):
print(name, score)zip stops at the shorter sequence, silently. If that would hide a bug, zip(a, b, strict=True) in Python 3.10+ raises instead.
Walk the items you already have
A for loop takes each item in turn and binds it to the loop variable.
Count with range, accumulate a total
range(n) produces 0 up to but not including n. Keeping a running total outside the loop is the workhorse pattern.
What a for loop walks
for colour in colours: reads better than for c in x: and costs nothing.range(5) is 0,1,2,3,4 - five numbers, stopping before 5. That matches how indexing works, so the two fit together.range(2, 10, 3) takes start, stop and step: 2, 5, 8.For Loops and range(): A Practical Guide
A for loop walks through a sequence, taking one item at a time. range() manufactures a sequence of numbers to walk through, without ever building the list.
What range actually produces
range does not build a list. It produces numbers one at a time as the loop asks for them, so range(10_000_000) uses a few dozen bytes rather than 400MB.
range(5) # 0, 1, 2, 3, 4 - stop only
range(2, 6) # 2, 3, 4, 5 - start and stop
range(0, 10, 2) # 0, 2, 4, 6, 8 - with a step
range(5, 0, -1) # 5, 4, 3, 2, 1 - counting down
list(range(3)) # [0, 1, 2] - materialise it if you mustThe stop value is always excluded, exactly as in slicing, and for the same reason: range(len(items)) then covers every valid index precisely once.
for _ in range(3): is the idiom for "do this three times" — the underscore signals that the value is deliberately unused.
break, continue, and the else nobody expects
for item in items:
if item.broken:
break # leave the loop entirely
if item.skip:
continue # jump to the next iteration
process(item)Python also allows an else on a loop, which runs only if the loop finished without breaking:
for user in users:
if user.email == target:
print("found")
break
else:
print("not found") # runs only if no break happenedIt reads oddly — "else" suggests a condition — but it removes the found = False flag variable that this pattern otherwise needs. Think of it as "no break".
Loops over dictionaries and nesting
prices = {"apple": 1.20, "pear": 0.90}
for key in prices: # keys by default
print(key)
for key, value in prices.items(): # both, and the usual choice
print(key, value)
for value in prices.values():
print(value)Nested loops run the inner loop completely for each pass of the outer one, so two loops over 1,000 items each is a million iterations. That multiplication is worth keeping in mind — most accidentally slow Python is a nested loop that could have been a dictionary or set lookup.
# slow: for each order, scan every customer
for order in orders:
for customer in customers:
if customer.id == order.customer_id:
...
# fast: build the lookup once
by_id = {c.id: c for c in customers}
for order in orders:
customer = by_id[order.customer_id]
Experiments to try
- Check the exclusive stop. Run range(5) and count the values. Five numbers, ending at 4 — the stop is a boundary, not a member.
- Count backwards. Try range(5, 0, -1), then range(5, 0) with no step. The second produces nothing at all, because the default step of +1 can never reach a smaller stop.
- Use enumerate. Loop over a list with and without enumerate and compare. The index comes for free rather than being maintained by hand.
- Try the loop else. Search for a value that exists, then one that does not, and watch when the else block fires.
Common mistakes
- Modifying a list while looping over it. Deleting items shifts the indices under the iterator and items get skipped. Loop over a copy (
for x in items[:]) or build a new list. - Using
range(len(x))when you do not need the index. Loop over the items, or useenumerate. - Expecting
rangeto include the stop value.range(1, 10)ends at 9. - Assuming
zipwarns about different lengths. It stops at the shortest without a word; passstrict=Truewhen that matters. - Reusing the loop variable afterwards. It survives the loop and holds the last value, which is legal and usually a mistake waiting to happen.
- Building a string with
+=in a loop. Strings are immutable, so every step copies the whole thing. Collect the pieces in a list and"".join(parts).
The short version
A for loop iterates over elements directly, and enumerate is how you get the index when you actually need it. range generates numbers lazily with an exclusive stop, so it costs nothing regardless of size. The loop else runs only when no break occurred, which makes it the clean way to handle a search that found nothing.
Comprehensions: the loop you write as an expression
When a loop's only job is to build a list, a comprehension says so more directly:
squares = [n * n for n in range(10)]
evens = [n for n in numbers if n % 2 == 0]
names = [u.name.title() for u in users if u.active]The same shape builds dictionaries and sets:
lengths = {word: len(word) for word in words}
initials = {name[0] for name in names} # a set, so no duplicatesAnd parentheses make a generator, which produces items lazily and is the right choice for large data or for feeding straight into sum, max or any:
total = sum(order.amount for order in orders) # no intermediate listUse a comprehension when the logic fits on one readable line. When it needs two conditions, a nested loop and a conditional expression, the plain loop is the clearer code.
What makes something iterable
A for loop is not a list feature. It works on anything that implements one small protocol, which is why the same syntax walks strings, files, dictionaries, sets, generators and objects from libraries you have never seen.
The loop calls iter() on whatever it was given to obtain an iterator, then calls next() on that repeatedly, and stops when StopIteration is raised. That is the whole mechanism. A class becomes iterable by defining __iter__, and there is no interface to declare or base class to inherit.
Two consequences follow that explain a lot of behaviour.
Some iterables can be walked repeatedly and some cannot. A list gives a fresh iterator each time, so two loops over it both see everything. A generator or a file object *is* its own iterator, holding a position, so a second loop sees nothing. The syntax is identical; the behaviour is not, and this is the single most common surprise when a loop over something that worked once produces nothing the second time.
**Anything iterable works with more than for.** list(), sum(), max(), sorted(), zip(), enumerate(), unpacking and comprehensions all consume the same protocol. Learning it once means a new iterable type needs no new knowledge to use.
Modifying a list while looping over it
This is the one for loop mistake that produces wrong output rather than an error:
items = [1, 2, 2, 3]
for x in items:
if x == 2:
items.remove(x)
print(items)[1, 2, 3]One of the twos survived. The iterator walks by position: after removing the first 2 at index 1, everything shifts left, so the next position the iterator visits is index 2 — which is now 3, and the second 2 was stepped over.
Nothing raises, and the result is plausible, which is what makes it dangerous. Removing every other item is a bug that looks like a subtle logic error rather than what it is.
Three fixes, in order of preference. Build a new list, usually with a comprehension: items = [x for x in items if x != 2], which is clearer as well as correct. Loop over a copy, for x in items[:], when the removal genuinely has to happen in place. Or walk backwards by index, so that shifting affects only positions already visited.
Dictionaries and sets are stricter about the same problem: changing their size during iteration raises RuntimeError rather than silently skipping. Lists get no such protection, because their iteration is positional.
The loop variable outlives the loop
A for loop does not create a scope, so its variable is still there afterwards holding the last value it took.
That is occasionally useful — after a search loop that ends with break, the variable holds the item that matched, which saves assigning it to something else. It is more often a quiet hazard.
Two ways it bites. A name reused for two loops in one function carries a value from the first into the second if the second never runs, which happens when its collection is empty. And reading the variable after a loop that iterated nothing raises NameError, because it was never assigned at all — the loop body simply did not execute.
Both mean the same thing in practice: do not depend on the loop variable after the loop unless you know the loop ran and you meant to. If the value matters, assign it to a clearly named variable inside the loop, where the intent is visible.
A comprehension does not have this behaviour. Its variable has its own scope and is gone the moment it finishes, which is one of the reasons the Python 3 change was worth making.
Reading a loop for cost
The most common performance problem in everyday Python is a loop, and the cost can be read off the page without measuring anything.
Count what runs per pass, and multiply by the passes. A loop over n items containing a dictionary lookup is n cheap operations. The same loop containing if x in some_list is n scans of that list, which is n times the list's length — the same code shape, an entirely different curve.
Three specific things to look for inside a loop body.
A membership test against a list. Converting that list to a set once, before the loop, is often the whole fix.
A search over another collection. Building a dictionary index once, outside the loop, replaces a scan per pass with a lookup per pass. This is the single most valuable transformation in ordinary Python code.
Work that does not depend on the loop variable. Anything computed the same way on every pass — opening a file, compiling a regular expression, building a constant list — belongs above the loop. The interpreter will not hoist it for you.
None of this matters at small sizes, and that is the trap: every one of these is invisible on ten items and decisive on ten thousand. The habit worth forming is to notice the shape while writing, because the fix is trivial then and an investigation later.
The idioms worth having in your fingers
A handful of loop shapes cover most of what anyone writes, and knowing them by sight saves reinventing each one.
for _ in range(n) repeats something n times, with the underscore saying the value is deliberately unused.
for i, item in enumerate(items, start=1) numbers output for a person to read, and the start keeps the adjustment out of the body.
for a, b in zip(first, second) walks two sequences together, and strict=True turns a length mismatch into an error rather than silent truncation.
for key, value in mapping.items() is the dictionary loop you almost always want; plain for key in mapping gives keys only.
for a, b in zip(items, items[1:]) pairs each item with the next, which is the shape for "compare consecutive values" without any index arithmetic.
for line in open_file reads one line at a time, holding a single line in memory regardless of file size.
for item in reversed(items) walks backwards without building a copy, unlike items[::-1].
What these have in common is that none of them maintains an index. That is the thread running through the whole topic: Python's for loop is a way of saying "for each of these", and every idiom above is a different answer to the question "each of what?" — rather than a different way of counting.
Questions people ask
How do I loop a fixed number of times? for _ in range(n):.
How do I loop backwards? for x in reversed(items):, or range(len(items) - 1, -1, -1) when you need indices.
Can I loop over two lists at once? zip(a, b), and zip(a, b, c) for three.
Is a for loop faster than a while loop? Usually, because the iteration is handled in C rather than by re-evaluating a condition in Python. Comprehensions are faster again for building lists.
How do I get both index and value? enumerate(items), with start=1 if you want human-friendly numbering.
What is the difference between range and a list? range generates numbers on demand and stores only start, stop and step; a list stores every element.
Can I loop over a number? No — integers are not iterable. range(n) is what turns a count into something to loop over.
What does for _ in ... mean? Nothing special to Python. _ is an ordinary name used by convention for a value you do not intend to read.
Recap in one screen
- Loop over items directly; use
enumeratefor positions andzipfor parallel sequences. range(start, stop, step)excludes the stop and produces values lazily.breakleaves the loop,continueskips an iteration, andelseruns only if nobreakhappened.- Nested loops multiply — replace an inner search with a dictionary or set lookup.
- Comprehensions are the idiomatic way to build a list, dict or set from an iterable.