Python – Iteration (for / while loops)

July 12, 20265 min readUpdated 8/20/2026

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.