Skip to content
VLSI Mentor

Python Part I · Module 10 · Loops

Loops That Never End, and Other Classic Mistakes

This chapter closes the module with the failures loops make available, and it is organised around a distinction worth more than the individual items: some loop bugs hang, and some lie. A loop that never ends is obvious the moment you look — the script does not return, the licence stays checked out, and somebody kills it. A loop that lies finishes in milliseconds and hands you a number that is plausible, well-formatted and wrong, and it will be in a report before anyone examines it. Four of the seven mistakes below are of the second kind. They are the reason this is a chapter rather than a warning box.

Foundation17 min readPythonPart IFundamentalsLoopsDebugging

Part I · Module 10 · Chapter 10.12 · Loops That Never End, and Other Classic Mistakes

1. Two Kinds of Loop Bug

Loops that hang. The condition never becomes false. The program produces nothing and holds whatever it was holding until a person intervenes. Costly, and unmistakable.

Loops that lie. The loop finishes normally and the result is wrong. No error, no warning, no delay — just a number or a list that nobody has a reason to doubt.

The first kind is in Sections 2 and 3. The rest are the second kind.

2. The Forgotten Increment

The canonical hang. The condition depends on attempts, and nothing changes attempts:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
attempts = 0
guard = 0

while attempts < 3:
    guard += 1
    if guard >= 6:
        print("after", guard, "passes, attempts is still", attempts)
        break

print("the guard stopped it; without the guard this never ends")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after 6 passes, attempts is still 0
the guard stopped it; without the guard this never ends

Six passes and attempts has never moved. It never will, because no line in the body touches it.

This usually happens by deletion rather than by omission — the increment was there, the body was reorganised, and it went with the lines around it. It also happens when the increment is inside a branch that does not always run, which is the same bug with a disguise.

The check from Chapter 10.5: point at the line that can make the condition false. If your finger does not land anywhere, the loop does not terminate.

3. Advancing in the Wrong Direction

A subtler hang: the body does change the value in the condition, and changes it the wrong way:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
countdown = 3
guard = 0

while countdown > 0:
    guard += 1
    countdown += 1
    if guard >= 5:
        print("countdown is now", countdown, "- moving away from zero")
        break
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
countdown is now 8 - moving away from zero

+= where -= was meant. The loop is not stuck — it is making steady progress away from its exit, which is worse, because a debugger stepping through it shows a value that is changing and a condition that is being re-evaluated.

Pointing at the advancing line is not enough. Check that it moves the condition towards false, not merely that it moves.

4. The Accumulator Reset Inside the Loop

Chapter 10.10 covered this at length; it is here because it is the most common lying loop:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
lines = ["UVM_ERROR a", "INFO b", "UVM_ERROR c"]

for line in lines:
    errors = 0
    if "UVM_ERROR" in line:
        errors += 1

print("errors:", errors)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
errors: 1

Two errors; the program says one. errors = 0 belongs at the same column as the for.

The giveaway when reading code: an assignment of an empty starting value (0, [], {}, "") inside a loop body is almost always wrong. It is right only when the value is genuinely per-pass — a per-test counter inside the outer loop of a nest, for example — and then it is worth a comment saying so.

5. Changing a List While Looping Over It

This one is silent, and it is the mistake most likely to survive review:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
tests = ["smoke", "skip_me", "skip_me", "reset"]

for test in tests:
    if test == "skip_me":
        tests.remove(test)

print("after:", tests)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after: ['smoke', 'skip_me', 'reset']

One "skip_me" was removed. One survived. No error.

The reason is positional. The loop tracks where it is by index. At position 1 it finds "skip_me" and removes it, so everything shifts left — the second "skip_me" moves into position 1, which the loop has already passed. Next pass it looks at position 2, which is now "reset".

Removing an item from a list while iterating it skips the item after it, every time. With consecutive matches, that means missing exactly the ones that were adjacent.

Two fixes, both correct:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tests = ["smoke", "skip_me", "skip_me", "reset"]

for test in list(tests):
    if test == "skip_me":
        tests.remove(test)

print("copy:", tests)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
copy: ['smoke', 'reset']

list(tests) makes a separate list to iterate, so removals from the original cannot disturb the walk.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tests = ["smoke", "skip_me", "skip_me", "reset"]

kept = []

for test in tests:
    if test != "skip_me":
        kept.append(test)

print("rebuild:", kept)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rebuild: ['smoke', 'reset']

Building a new list is the better habit. It is the accumulator pattern from Chapter 10.10, it never mutates what it is reading, and it states the rule positively — keep the ones that are not skipped — rather than as a sequence of removals.

6. Changing a Dictionary While Looping Over It

The same mistake, on a dictionary, behaves completely differently:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
status = {"smoke": "PASS", "reset": "FAIL", "burst": "FAIL"}

for key in status:
    if status[key] == "FAIL":
        del status[key]

print(status)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RuntimeError: dictionary changed size during iteration

Python detects it and refuses.

That difference is worth holding on to. A dictionary knows its size at the start of iteration and checks it; a list is walked by position and has no such check, so the same error is loud on one collection and silent on the other. Meeting the dictionary version first leaves people believing Python guards against this in general. It does not.

The same two fixes apply:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
status = {"smoke": "PASS", "reset": "FAIL", "burst": "FAIL"}

for key in list(status):
    if status[key] == "FAIL":
        del status[key]

print(status)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{'smoke': 'PASS'}

list(status) takes a snapshot of the keys — Chapter 10.3 established that looping a dictionary gives keys — so the deletions cannot disturb the walk.

7. Using the Loop Variable After the Loop

Chapter 10.1 established that the loop variable survives, holding the last item. Relying on that is a bug in two ways.

First, after an empty collection it does not exist at all:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
failed = []

for test in failed:
    pass

print("last test was", test)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
NameError: name 'test' is not defined

Second — and this one is silent — after a search that found nothing, the loop variable holds the last item examined, which is indistinguishable from having found it:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
tests = ["smoke", "reset", "burst"]

for test in tests:
    if test == "cdc":
        break

print("result:", test)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
result: burst

Nothing matched "cdc". The program reports burst, which is simply the last thing it looked at. Chapter 10.8's loop else, or an explicit flag, is how to say not found — the loop variable cannot.

8. Off-by-One With range()

Two shapes, both common:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
tests = ["smoke", "reset", "burst"]

for i in range(1, len(tests)):
    print(i, tests[i])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1 reset
2 burst

That is correct if skipping the first item was intended. Written by someone counting from one out of habit, it silently drops tests[0] from every report.

And the version that raises:

Azvya Education Pvt. Ltd.VLSI Mentor
BROKEN ON PURPOSE
tests = ["smoke", "reset", "burst"]

for i in range(len(tests) + 1):
    print(tests[i])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
IndexError: list index out of range

Both disappear when the index does. for test in tests: cannot skip the first item or run past the last, which is Chapter 10.4's argument restated as a defect class.

9. Two More From Earlier Chapters

A zip() of mismatched lengths truncates silently and misaligns every pair after the mismatch — Chapter 10.9, Section 8. strict=True turns it into a ValueError.

A string built with += in a loop leaves a trailing separator every time — Chapter 10.10, Section 6. Collect into a list and .join() it.

10. The Checklist

Before a loop goes into anything that runs unattended:

CheckLooking for
Can the condition become false?point at the line; a while with no advance hangs
Does the advance move towards the exit?+= where -= was meant
Is the accumulator created outside the loop?an empty value assigned inside the body
Does the body modify the collection being looped?.remove(), .pop(), del on the loop's own collection
Is the loop variable used after the loop?it is the last item, or undefined
Is there a range(len(...))?usually wants the collection, or enumerate()
Are zipped collections the same length?add strict=True
Is a while that waits on the outside world bounded?add a counter and a limit
For a nest, what is len(a) * len(b)?print it before submitting

11. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# A
items = [1, 2, 2, 3]
for n in items:
    if n == 2:
        items.remove(n)
print("A:", items)

# B
total = 0
for n in [1, 2, 3]:
    total = 0
    total += n
print("B:", total)

# C
names = ["clk", "rst"]
for name in names:
    if name == "data":
        break
print("C:", name)

# D
kept = []
for n in [1, 2, 2, 3]:
    if n != 2:
        kept.append(n)
print("D:", kept)

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: [1, 2, 3]
B: 3
C: rst
D: [1, 3]

A — one 2 survives. Section 5's skip, with numbers.

B — 3, not 6. The reset is inside, so only the last item contributed.

C — rst is the last name examined, not a match. Section 7.

D — the rebuild version of A, and the one to write.

12. Exercises

Exercise 1 — find the hang. For each, say whether it terminates and why: (a) n = 0 / while n < 5: / print(n) (b) n = 10 / while n > 0: / n -= 2 (c) n = 1 / while n < 100: / n *= 2 (d) n = 1 / while n > 0: / n += 1.

Exercise 2 — add a guard. Take the loop from Exercise 1(a) and add a guard that stops it after five passes and prints the stuck value. Explain in one sentence why the guard is not a fix.

Exercise 3 — the wrong direction. Write a countdown that uses += by mistake, bound it with a guard, and print the value at the point the guard fires. Say what a debugger would show you and why it is misleading.

Exercise 4 — the silent skip. Run Section 5's broken program with ["a", "x", "x", "x", "b"]. Predict how many "x" survive before running. Explain the number you get.

Exercise 5 — both fixes. Write the copy fix and the rebuild fix for Exercise 4's data. Say which leaves the original list intact and why that might matter.

Exercise 6 — loud or silent. Write the deletion-during-iteration mistake once on a list and once on a dictionary with the same logical content. Record what each does. Explain in two sentences why Python treats them differently.

Exercise 7 — not found. Write a search over ["clk", "rst"] for "data" that reports it correctly, using a loop else. Then write the version that uses the loop variable and show what it wrongly reports.

Exercise 8 — review a loop. Apply the Section 10 checklist to this loop and list every problem you find, then rewrite it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
lines = ["# h", "ERROR a", "ERROR b", "INFO c"]
for i in range(1, len(lines) + 1):
    count = 0
    if "ERROR" in lines[i]:
        count += 1
        lines.remove(lines[i])
print(count, line)

13. Summary

  • Loop bugs divide into those that hang and those that lie. The second kind is more expensive, because the answer looks fine.
  • The forgotten increment: nothing in the body changes what the condition depends on. Point at the line, or the loop never ends.
  • Advancing the wrong way is worse than not advancing — the value moves, so it looks like progress.
  • An accumulator created inside the loop is reset every pass and reports the last item's contribution.
  • An empty starting value assigned inside a loop body is almost always wrong.
  • Removing from a list while looping over it skips the next item, silently, because the walk is positional and everything shifts left.
  • The same mistake on a dictionary raises RuntimeError. Loud on one collection, silent on the other — Python does not protect you in general.
  • Fix it by looping over a copy, or better, by building a new collection.
  • The loop variable after a loop is the last item examined — undefined if the collection was empty, and misleading after a failed search. Use a loop else or a flag.
  • range(len(...)) invites both off-by-one shapes. Loop the collection, or use enumerate().
  • Run the checklist before anything goes into a nightly job.

That closes Module 10. You can now repeat work over any collection, count passes without a collection, stop early, skip items, report which exit a loop took, number and pair items without manual indices, accumulate results of five different kinds, expand a grid of combinations, and recognise the nine ways a loop goes wrong.

Every loop so far has had its body written inline, and the next module is about what happens when that body is worth a name — and why a script that repeats the same six lines three times is one bug fix away from being wrong in two places.

Where this fits

Part of the Python curriculum.