Loops That Do Not Run Away
Repeating work over a collection, repeating until something changes, and the loops that never finish. enumerate and zip, and why you rarely need an index in Python.
Repeating work over a collection, repeating until something changes, and the loops that never finish. enumerate and zip, and why you rarely need an index in Python.
You have a list of four hundred photographs and a function that processes one. Writing that function call four hundred times is not a plan. You need a way to say "do this to each of them", and then a way to say "keep going until something changes".
Those are the two loops Python gives you, and they are for genuinely different situations. By the end of this lesson you will know which to reach for, how to stop one early, and how to avoid the one that never stops — which is the mistake that teaches everyone where the interrupt key is.
The for loop walks through a collection, one item at a time:
photos = ["dawn.jpg", "tram.jpg", "square.jpg"]
for photo in photos:
print(f"Processing {photo}")Processing dawn.jpg
Processing tram.jpg
Processing square.jpgphoto is a name you choose. On each pass round the loop it is
attached to the next item, and the indented block runs again.
When the collection is exhausted, the loop ends and the program
continues below.
It works on anything you can go through — a list, a tuple, a set, a dictionary, a string, the lines of a file:
for character in "Lisbon":
print(character)
for name, count in photo_counts.items():
print(f"{name}: {count}")
for line in open("sales.txt"):
print(line.strip())That is the single most useful thing about the for loop:
learn it once, and it works on every collection you will ever
meet, including ones that do not exist yet.
If you want numbers rather than items, range produces them:
for i in range(5):
print(i) # 0 1 2 3 4 - starts at 0, stops BEFORE 5range(5) # 0, 1, 2, 3, 4
range(2, 6) # 2, 3, 4, 5
range(0, 10, 2) # 0, 2, 4, 6, 8 - step of 2
range(5, 0, -1) # 5, 4, 3, 2, 1 - counting downrange stops before the end value, the same rule as slicing.
That is what makes range(len(photos)) line up exactly with the
valid positions.
But you should rarely write range(len(...)), and here is why.
Bad — looping over positions to get at the items.
for i in range(len(photos)):
print(f"{i + 1}. {photos[i]}")Good — asking for both at once.
for index, photo in enumerate(photos, start=1):
print(f"{index}. {photo}")The first version makes you manage an index you did not want,
and every use of it is a chance to write photos[i + 1] or
photos[1] by mistake — errors that either crash at the last
item or silently process the wrong one. enumerate hands you
the position and the item together, and start=1 handles the
human-numbering adjustment that the first version does with
arithmetic scattered through the body.
zip is the matching tool for walking two collections together:
names = ["dawn.jpg", "tram.jpg"]
sizes = [340, 128]
for name, size in zip(names, sizes):
print(f"{name} is {size}KB")zip stops at the shorter one, which is worth knowing before it
silently drops your last three records.
A for loop needs to know what it is going through. Sometimes
you do not know how many times you will go round — you only know
what has to be true to stop.
answer = ""
while answer not in ("y", "n"):
answer = input("Process all photos? (y/n) ").lower().strip()
print(f"You said {answer}")while checks its condition, runs the block if it holds, then
checks again. It keeps going until the condition is false.
The rule for choosing is short enough to memorise.
You have a collection.
You know what you are going through, even if you do not know how many there are. The loop ends when the collection is exhausted, so it cannot run away.
You have a condition.
You do not know how many times you will go round — only what has to become true to stop.
Nothing ends it for you. That is what the next section is about.
Everyone writes one. Here is the classic:
count = 0
while count < 10:
print(count)
# forgot: count = count + 1The condition never becomes false, so the loop never ends. Press Ctrl + C to stop a runaway program — learn that now rather than during the incident.
The subtler version modifies the wrong thing:
photos_left = 10
while photos_left > 0:
process_next()
photos_left = 10 - processed_count # if this never changes...Two words change the flow inside a loop.
break leaves the loop immediately:
for photo in photos:
if photo.name == target:
print("Found it")
break # stop lookingcontinue skips the rest of this pass and goes to the next
item:
for photo in photos:
if photo.is_corrupt:
continue # nothing more to do with this one
process(photo)
archive(photo)continue is the tidier alternative to wrapping the whole body
in an if. Handling the uninteresting cases first and skipping
them keeps the main work at one level of indentation instead of
three, which matters more than it sounds once a loop body grows.
Both affect only the innermost loop they are in. In nested
loops, a break in the inner one leaves the inner one and
carries on with the outer.
The most common loop of all does not just act on each item — it accumulates a result.
total_size = 0
for photo in photos:
total_size = total_size + photo.size
print(total_size)Three variations you will write constantly:
# collect the ones that match
large = []
for photo in photos:
if photo.size > 1000:
large.append(photo)
# transform every one
names = []
for photo in photos:
names.append(photo.name.lower())
# tally by category
counts = {}
for photo in photos:
counts[photo.tag] = counts.get(photo.tag, 0) + 1All three are one skeleton with a different middle.
Start with an empty thing
0 for a total, [] for a collection, {} for a tally.
Before the loop, always.
Go through, and decide about each item
Keep it, change it, or count it. This is the only part that differs between the three examples above.
Add the result to the thing
total += x, results.append(x),
counts[k] = counts.get(k, 0) + 1.
Python has shorter ways to write the first two, and that is the subject of the very next lesson. Learn them in this form first — the short version is a convenience, and a convenience you do not understand is harder to debug than the long version you do.
Some accumulations already have built-in functions, and you should use those:
print(sum(sizes)) # add them all up
print(max(sizes)) # largest
print(min(sizes)) # smallest
print(len(photos)) # how many
print(any(p.is_corrupt for p in photos)) # is at least one?
print(all(p.is_processed for p in photos)) # are they all?A loop body can contain another loop. The inner one runs completely on every pass of the outer one:
for photographer in photographers:
for photo in photographer.photos:
process(photo)This is fine and often exactly right. The thing to be aware of is the arithmetic: a hundred photographers with a hundred photos each is ten thousand passes. Nesting a loop inside a loop multiplies the work, so a pair that is instant on test data can be slow on real data.
The version to watch for is a search inside a loop:
for photo in photos: # 400,000
if photo.name in processed_names: # scanning a list of 400,000
continueThat is the case the previous lesson's set was for. Making
processed_names a set turns billions of comparisons into
hundreds of thousands, with a one-word change.
THE TWO LOOPS
for item in collection: you have a collection
while condition: you have a condition
GOING THROUGH THINGS
for x in a_list:
for c in "text":
for k, v in a_dict.items():
for line in open(path):
range(5) 0 1 2 3 4 - stops BEFORE 5
range(2, 6) 2 3 4 5
range(0, 10, 2) 0 2 4 6 8
range(5, 0, -1) 5 4 3 2 1
enumerate(x) position and item <- not range(len(x))
enumerate(x, start=1) human numbering
zip(a, b) two at once; stops at the shorter
CONTROL
break leave the loop now
continue skip to the next item
else: runs only if no break happened
Ctrl+C stop a runaway program
BUILDING UP
total = 0 ... total += x
results = [] ... results.append(x)
counts = {} ... counts[k] = counts.get(k, 0) + 1
ALREADY BUILT IN
sum() max() min() len()
any(...) at least one is true
all(...) every one is true
sorted() reversed()
TRAPS
no counter change -> the loop never ends
removing while looping -> items get skipped, no error
`in` a list in a loop -> use a set
nesting multiplies -> 100 x 100 is 10,000You can now do something to every item in a collection, repeat until a condition changes, leave early, skip the uninteresting cases, and build up a result as you go. You also know the three loop bugs that produce wrong answers rather than errors — runaway loops, mutation during iteration, and a scan hidden inside a loop.
Next is Functions, Arguments and Return Values. Every loop body in this lesson was a piece of work you might want somewhere else too, and a function is how you give that work a name and use it from anywhere — which is what stops a growing program turning into one long transcript.
Before you move on, take the accumulation examples and write them against real data — the files in a folder, the lines of a text file. Then deliberately write a loop that removes items from the list it is walking, and watch it skip. That bug is invisible in review and obvious once you have caused it on purpose.