Before loops, printing hello ten times means ten lines — or nine lines and one copy-paste typo. Loops are also where habits from other languages sneak in, so this post starts where I wish I'd started: in Python you loop over things, not over counters.
names = ["Ada", "Grace", "Linus"]
for name in names:
print("hello", name)
# hello Ada
# hello Grace
# hello Linus
The loop walks the list and hands you one item per turn; name is a fresh label for the current one, and the whole thing reads like the sentence it is: for each name in names, say hello.
for n in range(1, 4):
print(n) # prints 1, 2, 3 — the stop value never prints
range(1, 4) means start at 1, stop before 4 — the stop is a fence you walk up to, never stand on. Everyone arriving from C or Java gets this wrong the same way: their loop instincts say "1 to 5" should be range(1, 5), and an iteration quietly vanishes. The honest translation of for (i = 0; i < n; i++) is range(n), full stop.
for i, name in enumerate(["Ada", "Grace", "Linus"], start=1):
print(i, name)
# 1 Ada
# 2 Grace
# 3 Linus
My old habit was for i in range(len(names)) and then names[i] — it works, but it's a translation, not Python. These days, catching myself writing range(len(...)) is almost always the sign that I wanted enumerate, or that I didn't need the index at all.
log_lines = [
"# build started",
"compiled main.py",
"ERROR: tests failed",
"cleaning up",
]
for line in log_lines:
if line.startswith("#"):
continue # skip this turn, go to the next
if "ERROR" in line:
break # abandon the whole loop
print(line)
# compiled main.py
continue abandons the current turn and jumps to the next one; break abandons the loop entirely. Here the comment line is skipped, the compile line gets printed, and the ERROR line ends the run before cleaning up ever gets a turn.
queue = ["email Ada", "resize photos", "back up blog"]
while queue: # keep going while the list isn't empty
task = queue.pop(0) # take the first task off
print("doing:", task)
attempts = 0
while attempts < 3:
print("retrying") # attempts never changes: this never stops
# the fix is one line somewhere inside: attempts += 1
A while loop repeats as long as its condition is true, so something inside the loop has to move the guard. In the queue version, every pop() shrinks the list until it's empty, which Python reads as false, and the loop ends. Forget to move the guard and you've written the classic infinite loop — if you ever meet one live, Ctrl+C stops it in the terminal.
Which one when? for when you know what you're walking over — a list, a file, a range. while when you don't know how many turns it will take: until the user types quit, until the download finishes, until the queue drains. Nine of my ten loops are for; the while loop is for the scripts that sit in a terminal and wait for the world.