Skip to content
VLSI Mentor

Python Part I · Module 10 · Loops

while Loops, Polling and Retrying

A for loop needs a collection, and a collection answers the question how many passes before the loop starts. Plenty of real work cannot answer it. A simulation is running and will finish at some point; a licence server is busy and may free up; a flaky network step failed and might succeed on the second attempt. In each case the number of passes is not a property of your data — it is a property of something outside your program, and you find out only by checking again. That is what a while loop is for, and it comes with one responsibility a for loop does not have: because nothing limits it in advance, you have to limit it yourself.

Foundation16 min readPythonPart IFundamentalsLoopswhile

Part I · Module 10 · Chapter 10.5 · while Loops, Polling and Retrying

1. When There Is No Collection

Every loop so far began with something to walk: a list of tests, a range of seeds, the lines of a log. Here is a problem with no such thing.

A job has been submitted. You want to carry on when it finishes. How many times will you have to check? You cannot know — it depends on the job, the machine and the queue. There is no collection of checks to loop over, because the checks are generated by the situation rather than by your data.

What you can say is the condition under which you keep going: while the job is still running, check again.

2. The Statement

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
remaining = 3

while remaining > 0:
    print("remaining:", remaining)
    remaining -= 1

print("done, remaining =", remaining)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
remaining: 3
remaining: 2
remaining: 1
done, remaining = 0

The shape is while <condition>: followed by an indented body — the same block rule as Chapter 9.2, and visually almost the same as an if. The difference is what happens at the bottom of the body: control goes back to the condition instead of continuing past it.

3. The Condition Is Re-Tested Before Every Pass

This is the whole of the statement, and it is worth stating against the thing it most resembles.

Chapter 9.7 established that an if evaluates its condition once, when execution reaches it, and never again. Here are the two statements over the same data:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
remaining = 3

if remaining > 0:
    print("if: remaining is", remaining)
    remaining -= 1

print("if finished with remaining =", remaining)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if: remaining is 3
if finished with remaining = 2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
remaining = 3

while remaining > 0:
    print("while: remaining is", remaining)
    remaining -= 1

print("while finished with remaining =", remaining)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
while: remaining is 3
while: remaining is 2
while: remaining is 1
while finished with remaining = 0

Identical conditions, identical bodies. The if ran its body once and moved on. The while ran it until the condition stopped being true, re-evaluating remaining > 0 before each pass — four evaluations in total, the last of which was false.

4. Something in the Body Must Change the Answer

A for loop ends because the collection runs out. A while loop ends only if the condition becomes false, and the only thing that can make that happen is the body.

In Section 2 that was remaining -= 1. Remove it and the condition is true forever, because nothing it depends on ever changes.

The question to ask of every while loop you write or read is: what in this body can make the condition false? If you cannot point at a specific line, the loop does not terminate.

For while remaining > 0: the answer is remaining -= 1, and you can check it: remaining strictly decreases every pass and the condition is a lower bound, so it must eventually be reached.

5. The Condition Runs One More Time Than the Body

A small fact that makes off-by-one reasoning about while loops reliable: for n passes of the body, the condition is evaluated n + 1 times. The extra one is the evaluation that finally comes out false and ends the loop.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
checks = []
remaining = 3

while remaining > 0:
    checks.append(remaining)
    remaining -= 1

print("body ran", len(checks), "times for", checks)
print("the condition was evaluated", len(checks) + 1, "times")
print("the last evaluation was", 0, "> 0 ->", 0 > 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
body ran 3 times for [3, 2, 1]
the condition was evaluated 4 times
the last evaluation was 0 > 0 -> False

Three passes, four evaluations. This is why the value a while loop leaves behind is the first value that failed the test, not the last one that passed it — remaining ends at 0, which the body never saw. Code after a while loop that uses the loop's variable is reading that failing value, and expecting the last successful one is a reliable source of off-by-one bugs.

6. Polling: Waiting for Something to Finish

Here is the first real use. A job is checked repeatedly until it reports something other than RUNNING:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
polls = ["RUNNING", "RUNNING", "RUNNING", "DONE"]

state = "RUNNING"
checks = 0

while state == "RUNNING":
    state = polls[checks]
    checks += 1
    print("check", checks, "->", state)

print("job finished after", checks, "checks")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
check 1 -> RUNNING
check 2 -> RUNNING
check 3 -> RUNNING
check 4 -> DONE
job finished after 4 checks

Four passes. Nothing in the program decided that number — the data did, and with a different polls list it would be a different number. That is exactly the situation a for loop cannot express.

7. Retrying: The Bounded Attempt

The second use is a step that fails for reasons that have nothing to do with your code — a licence checkout that collided, a filesystem that was briefly unavailable. Trying again is reasonable; trying forever is not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
attempt_results = ["timeout", "timeout", "ok"]

attempts = 0
ok = False

while attempts < 3 and not ok:
    result = attempt_results[attempts]
    attempts += 1
    print("attempt", attempts, "->", result)
    if result == "ok":
        ok = True

print("ok =", ok, "after", attempts, "attempts")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
attempt 1 -> timeout
attempt 2 -> timeout
attempt 3 -> ok
ok = True after 3 attempts

The condition has two parts and they do different jobs. not ok is the success exit — stop as soon as it works. attempts < 3 is the bound — stop regardless after three tries. A loop with only the first can run forever; a loop with only the second always uses all three attempts even after succeeding.

The failing case matters as much as the succeeding one, and the loop handles it without a new branch:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
attempt_results = ["timeout", "timeout", "timeout"]

attempts = 0
ok = False

while attempts < 3 and not ok:
    result = attempt_results[attempts]
    attempts += 1
    print("attempt", attempts, "->", result)
    if result == "ok":
        ok = True

print("ok =", ok, "after", attempts, "attempts")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
attempt 1 -> timeout
attempt 2 -> timeout
attempt 3 -> timeout
ok = False after 3 attempts

The loop ended because the bound was reached, and ok is False. After a bounded retry, the flag tells you which exit you took — and the code after the loop must check it, because "the loop finished" does not mean "it worked".

8. A Condition With Three Parts

Real waiting usually has more than one reason to stop. A job can finish, your patience can run out, or the queue can tell you something that means waiting is pointless:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
states = ["RUNNING", "RUNNING", "SUSPENDED"]

state = "RUNNING"
checks = 0
aborted = False

while state == "RUNNING" and checks < 5 and not aborted:
    state = states[checks]
    checks += 1
    if state == "SUSPENDED":
        aborted = True

print("state:", state, "| checks:", checks, "| aborted:", aborted)

if aborted:
    print("the queue suspended the job — not waiting further")
elif state == "RUNNING":
    print("gave up waiting after the bound")
else:
    print("job reached", state)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
state: SUSPENDED | checks: 3 | aborted: True
the queue suspended the job — not waiting further

Three exits, and the code after the loop distinguishes them with the chain from Chapter 9.1. That reporting block is not optional decoration — a loop with three ways out that prints one message is a loop whose log entry cannot be acted on.

Note the condition is read left to right with short-circuit evaluation (Chapter 9.3), so states[checks] is only reached when the bound still holds. Ordering the parts so the cheap safety checks come first is the same discipline that made the guard work there.

9. Always Bound It

The rule for anything that runs unattended: a while loop whose condition depends on the outside world gets a counter and an upper limit, every time.

The reason is operational rather than aesthetic. A for loop with a bug produces a wrong answer, which you find in the morning and fix. An unbounded while loop that never becomes false does not produce anything — it holds its machine, its simulator licence and its slot in the queue until a person notices and kills it, and a nightly regression that hangs at 02:00 has lost the whole night by 09:00.

A bound costs two lines:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
state = "RUNNING"
checks = 0
polls = ["RUNNING", "RUNNING", "RUNNING", "DONE"]

while state == "RUNNING" and checks < 100:
    state = polls[checks]
    checks += 1

print("state:", state, "after", checks, "checks")

if state == "RUNNING":
    print("gave up waiting — the job is still running")
else:
    print("job reached state", state)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
state: DONE after 4 checks
job reached state DONE

The bound did not trigger here, and that is the normal case. It exists for the night when the queue system wedges and the job never changes state — on that night, this script gives up after a hundred checks and says so, instead of waiting until someone arrives.

10. A Condition That Starts False Runs Zero Passes

Like a for loop over an empty collection, a while whose condition is false at the start simply does nothing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
queued = 0

while queued > 0:
    print("draining")
    queued -= 1

print("nothing to drain")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nothing to drain

The condition is tested before the first pass, not after it, so there is no guaranteed minimum of one. This is worth knowing because the mental picture of "do this, then check" is wrong — it is "check, then maybe do this".

11. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# A
n = 0
while n < 3:
    print("A:", n)
    n += 1

# B
n = 5
while n < 3:
    print("B:", n)
    n += 1
print("B finished, n =", n)

# C
total = 0
n = 1
while n <= 4:
    total += n
    n += 1
print("C total:", total)

# D
word = "axi"
i = 0
while i < len(word):
    print("D:", word[i])
    i += 1

The verified output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 0
A: 1
A: 2
B finished, n = 5
C total: 10
D: a
D: x
D: i

A — three passes; the condition is false when n reaches 3.

B — zero passes. The condition was already false, so the body never ran and n is untouched.

C — 1 + 2 + 3 + 4. The <= makes 4 inclusive, which is a deliberate contrast with range(1, 5) doing the same job in Chapter 10.4.

D — this works, and it is the wrong tool. Walking a collection by index with a while is what for ch in word: does in one line with no counter to get wrong. Chapter 10.6 is about choosing between them.

12. Exercises

Exercise 1 — name the advance. For each loop, point at the line that can make the condition false, or say that none can: (a) while n < 10: with n += 2 (b) while not done: with print(done) (c) while attempts < 3: with attempts = 0 inside (d) while len(queue) > 0: with queue.pop().

Exercise 2 — if versus while. Write the same three-line body under an if remaining > 0: and under a while remaining > 0: with remaining = 2. Run both, and explain the difference in output in two sentences using the word "re-evaluated".

Exercise 3 — bound an unbounded loop. The polling loop in Section 5 has no bound. Add one of 2 checks, run it against the same polls list, and print whether it gave up or finished. Say what the output tells the person reading the log.

Exercise 4 — which exit. Using Section 6's retry, add a line after the loop that prints either succeeded on attempt N or gave up after N attempts. Verify it with both the succeeding and the failing attempt_results.

Exercise 5 — zero passes. Write a while loop that you expect to run three times, then change one value so it runs zero times without changing the body. Explain which part of the loop decided that.

Exercise 6 — count the evaluations. For n = 3 and while n > 0: with n -= 1, state exactly how many times the condition is evaluated and how many times the body runs. Explain why the two numbers differ by one.

Exercise 7 — drain a queue. Given queue = ["j1", "j2", "j3"], use a while loop and .pop() to process and remove jobs until the queue is empty, printing each. Then say in one sentence why a for loop over queue would have been a bad idea here.

Exercise 8 — two-part condition. Write a retry that stops on success or after 5 attempts, using one while condition. Then write the version with only the bound and the version with only the success test, and describe in one sentence each what goes wrong.

13. Summary

  • A while loop repeats as long as a condition is true, and is for situations where the number of passes is not known in advance.
  • The condition is re-evaluated before every pass — the one real difference from an if, which evaluates once.
  • Everything from Module 9 applies to the condition: comparisons, and/or/not, truthiness, short-circuit.
  • Something in the body must be able to make the condition false. If you cannot point at the line, the loop does not terminate.
  • Polling waits for an outside state to change; the number of checks is decided by the situation, not by your data.
  • A bounded retry uses a two-part condition: a success exit and an attempt limit.
  • After a bounded loop, check which exit you took — finishing is not succeeding.
  • Always bound a while that depends on the outside world. A hung nightly run costs the whole night; a bound costs two lines.
  • A condition that is false at the start runs the body zero times — it is "check, then maybe do", not "do, then check".
  • The condition is evaluated one more time than the body runs, so the value left behind is the first one that failed the test, not the last that passed it.
  • A condition may have several parts — a success exit, a bound, and an abort flag — and the code after the loop must say which of them ended it.

The next chapter puts the two loop statements side by side and settles which problem each one is for.

Where this fits

Part of the Python curriculum.