Python's for loop is not the one you know from C or Java. It does not count — it walks
a collection directly, and once that clicks a lot of code gets shorter.
for walks a collection, not an index
accounts = ["Checking", "Savings", "Credit"]
for account in accounts:
print(account)
# Output: Checking
# Output: Savings
# Output: Credit
No counter, no length, no accounts[i]. The loop asks the list for its items one at a
time. That means the same loop works over a string, a file, a dictionary, a set or anything else that
can be iterated — you do not learn a new form for each.
If you find yourself writing for i in range(len(items)) just to reach
items[i], you are writing the C loop in Python. There is nearly always a better way, and
the next two sections are it.
range, when you really do want numbers
for n in range(3):
print(n)
# Output: 0
# Output: 1
# Output: 2
print(list(range(1, 4))) # Output: [1, 2, 3]
print(list(range(0, 10, 3))) # Output: [0, 3, 6, 9]
print(list(range(3, 0, -1))) # Output: [3, 2, 1]
range(stop) starts at zero. range(start, stop) includes the start and
excludes the stop — the same half-open rule as slicing, which is why
range(len(x)) lines up with the valid indices. The third argument is the step and may be
negative.
range does not build a list. It generates the numbers as you ask for them, so
range(10_000_000) costs nothing until you loop over it.
enumerate, when you want the index too
accounts = ["Checking", "Savings"]
for position, account in enumerate(accounts, start=1):
print(f"{position}) {account}")
# Output: 1) Checking
# Output: 2) Savings
This is the answer to "but I need the index". enumerate hands you both, and
start=1 is what you want for anything a person reads — menu options, line numbers,
rankings.
zip, when you have two collections
names = ["Checking", "Savings"]
balances = [1250.00, 8400.50]
for name, balance in zip(names, balances):
print(f"{name:<10}{balance:>10,.2f}")
# Output: Checking 1,250.00
# Output: Savings 8,400.50
zip stops at the shortest input, silently. That is convenient until it hides a bug, so
pass strict=True (3.10 and later) when the lists are supposed to be the same length and
you want to hear about it if they are not.
while, when you do not know how many
A for loop runs once per item. A while loop runs until a condition goes
false, which is what you want when the number of turns is not known in advance:
attempts = 0
password = ""
while password != "correct-horse" and attempts < 3:
attempts += 1
password = "correct-horse" if attempts == 2 else "wrong"
print(f"took {attempts} attempts") # Output: took 2 attempts
Retry loops, menu loops and "keep asking until the input is valid" are the honest uses. Something has to change inside the loop or it never ends, and forgetting that is the classic infinite loop — Ctrl-C stops a runaway.
Whenever you are walking a collection, use for. A while loop with a manual
index is a for loop with extra opportunities to be wrong.
Looping over a dictionary
Iterating a dict gives you its keys, which surprises people who expected the values:
balances = {"Checking": 1250.00, "Savings": 8400.50}
for name in balances:
print(name)
# Output: Checking
# Output: Savings
for name, balance in balances.items():
print(f"{name}: {balance:,.2f}")
# Output: Checking: 1,250.00
# Output: Savings: 8,400.50
.items() is what you almost always want — it yields key/value pairs and the loop
unpacks them into two names. .values() gives just the values when you do not need the
keys. Since 3.7 the order is the order you inserted them, so this is repeatable rather than arbitrary.
Dictionaries & Sets goes further.
break and continue
transactions = [100, -25, 0, 250, -40]
for amount in transactions:
if amount == 0:
continue # skip this one, keep going
if amount < -30:
print("large withdrawal, stopping")
break # leave the loop entirely
print(amount)
# Output: 100
# Output: -25
# Output: 250
# Output: large withdrawal, stopping
continue skips the rest of this turn. break abandons the loop. Both apply
to the innermost loop only — Python has no labelled break, and needing one is a strong hint the inner
loop should be a function you can return from.
The loop-else nobody knows about
A loop can have an else, and it runs only if the loop finished without
hitting break:
accounts = [{"id": 1, "type": "Checking"}, {"id": 2, "type": "Savings"}]
for account in accounts:
if account["type"] == "Credit":
print("found it")
break
else:
print("no credit account") # Output: no credit account
It is the search idiom: find the thing and break, or fall through to the "not found"
branch. Without it you need a found = False flag and an if not found:
afterwards.
The keyword is badly chosen — read it as "no break" rather than "else" and it makes sense. Because it is obscure, add a comment when you use it.
Do not change a list while looping over it
amounts = [100, -25, -40, 250]
for amount in amounts:
if amount < 0:
amounts.remove(amount)
assert amounts == [100, 250], f"got {amounts}"
That assertion fails: the result is [100, -40, 250], with a negative number still in
it. Removing -25 shifted -40 back into the slot the loop had just left, while
the loop's internal position moved forward — so -40 was never examined.
What makes this genuinely dangerous is that it does not always show. Change the input to
[100, -25, 250, -40] and the same broken loop returns the right answer, because the
skipped element happened to be the last one. A bug that passes your test and fails in production is
worse than one that always fails.
Build a new list instead — which is exactly what a list comprehension is for:
amounts = [100, -25, -40, 250]
amounts = [a for a in amounts if a >= 0]
print(amounts) # Output: [100, 250]
That is the next post.