skip to content

Which Python while loops are better rewritten as for loops, and why?

level: middleimportance: nice to knowfreq 28%

answer

  1. Who decides the number of passes?
  2. Can you name the iterable in advance?
  3. The iterator owns the advancing step
  4. Two-argument iter takes a sentinel
  5. Convergence loops stay while loops

basics

~20 s

Rewrite when the number of passes is dictated by an existing iterable or a repeated call rather than by state the body computes. The iterator advances for you, so there is no step to forget and no element to skip.

solid answer

~40 s

Reach for `for` whenever something else already knows how many passes there are. Walking a sequence by a hand-maintained cursor is the classic rewrite: the `while` version carries an increment you can forget or skip on a branch, and the `for` version cannot have that bug because the iterator protocol does the advancing. A sentinel-controlled `while True:` over a repeated zero-argument call has a direct `for` spelling too — `iter(callable, sentinel)` returns an iterator that calls the function until its result equals the sentinel, so `for line in iter(stream.readline, ""):` states the terminator in the header. `itertools.takewhile` and `itertools.count` cover related shapes. Keep `while` when the stop test depends on state the body produces: converging on a tolerance, exhausting a computed budget, driving a state machine, or an intentionally unbounded service loop.

code

python · 5 lines
python
import io

stream = io.StringIO("alpha\nbeta\ngamma\n")
for line in iter(stream.readline, ""):
    print(line.rstrip("\n"))

go deeper

for a junior

Default to for when you are visiting the items of something that already exists, and reserve while for loops whose ending depends on a condition you compute as you go.

for a middle

Explain what the rewrite buys: the iterator owns the position, so the forgotten or conditional increment cannot happen. Know the two-argument iter() spelling of a sentinel loop.

for a senior

Judge the cases that resist rewriting — convergence, budgets, state machines — and push back on for loops that iterate a counter and then break, which hides the real stop test from the header.

for a principal

Frame the guidance for the codebase: prefer the statement that puts the termination argument in the header, and treat a repeated hand-rolled traversal as a signal to give the structure an __iter__ instead.

### The distinction that decides it `for` and `while` answer different questions. `for` asks *what do I iterate over* — the pass count is owned by an iterable, and the loop's job is to visit each item. `while` asks *when do I stop* — the pass count is not known up front and is decided by state the body computes. Choosing the wrong one is not a style quibble; each statement removes a class of bug the other allows. ### The rewrite that removes a bug class The most common needless `while` walks a sequence with a hand-maintained cursor: ```python i = 0 while i < len(records): process(records[i]) i += 1 ``` Three separate things must stay in agreement: the initial value, the bound, and the increment. Forget the increment and the loop never ends; put the increment inside an `if` and some elements are visited twice; adjust the bound and you have an off-by-one. Written as `for record in records:` all three vanish, because the iterator protocol owns the position: `for` calls `__next__` on an iterator obtained from the iterable, binds the result, and stops when `StopIteration` is raised. There is nothing left to get wrong about advancing. The same applies to walking a linked structure only if the structure exposes iteration; where it does not, a `while` with an explicit cursor is the honest spelling, and writing `__iter__` for the structure is the real fix if the traversal appears more than once. ### Sentinel loops have a `for` spelling The body-first read loop — `while True:`, read, test, `break` — has a direct translation many Python programmers have never met. The two-argument form of `iter()` takes a callable and a sentinel and returns an iterator that calls the callable with **no arguments** on each pass, raising `StopIteration` as soon as the result equals the sentinel: ```python for line in iter(stream.readline, ""): print(line.rstrip("\n")) ``` The terminator now sits in the loop header, where a reader looks for the stop condition, instead of buried in the body. Note that the sentinel value itself is **not** yielded — it ends the iteration — and that the callable must take no arguments, so a call needing arguments has to be wrapped in a small closure or a `functools.partial` first. The same idiom reads a binary file in fixed-size blocks, drains a queue-like object with a non-blocking getter, or pulls rows from a cursor-style API until it returns its own end marker. `itertools` covers neighbouring shapes: `itertools.count()` gives an unbounded counting iterable when you want a `for` with no upper bound, `itertools.repeat(None, n)` expresses "do this n times" without a throwaway variable, and `itertools.takewhile(predicate, iterable)` turns "consume while this holds" into a `for` header. ### Loops that should stay `while` Rewriting reflexively is its own error. Keep `while` when the termination test genuinely depends on what the body produced: - **Convergence.** Refining an estimate until the change falls under a tolerance has no iterable behind it; the next value is computed from the last. ```python x = 2.0 while abs(x * x - 2.0) > 1e-12: x = (x + 2.0 / x) / 2.0 ``` - **A computed budget.** Consuming until a remaining allowance is exhausted, where each pass subtracts an amount only known after the work. - **State machines.** The next state is chosen inside the body; there is no sequence to iterate. - **Deliberately unbounded loops.** A REPL, an interactive prompt, or a supervisor running until told to stop is an honest `while True:`. A useful test before rewriting: can you name the iterable *without running the body*? If yes, `for` is available and almost always clearer. If naming it requires knowing what the body computes, you are looking at a `while`, and the right investment is making its termination argument visible rather than forcing it into a `for`. ### Why it matters beyond taste The two statements localise different information. A `for` header tells the reader the extent of the loop in one line and guarantees termination whenever the iterable is finite. A `while` header tells the reader the *condition*, and the guarantee has to be reconstructed from the body. Choosing `for` where it fits therefore removes work from every future reader, not just from the person who forgot an increment. Conversely, disguising a genuine `while` as a `for` — iterating over a counting iterable and breaking out on a computed condition — takes the stop test *out* of the header and leaves the worst of both.

  • How does the two-argument form of `iter()` decide when to stop?
    `iter(callable, sentinel)` returns an iterator that invokes the callable with no arguments on each pass and raises `StopIteration` as soon as the returned value equals the sentinel. The sentinel is not yielded. Because the callable takes no arguments, anything needing them must be wrapped in a closure or a `functools.partial` first.
  • Which loops should stay `while` loops rather than becoming `for` loops?
    Ones whose stop test depends on state the body produces: refining an estimate until the change is under a tolerance, consuming a budget reduced by amounts known only after the work, driving a state machine, or an intentionally unbounded service loop. If you cannot name the iterable without running the body, `for` does not fit.

saying these in an interview costs you the question

  • Rewrites every while as a for, convergence loops included
  • Walks a list by hand-maintained index for no reason
  • Forgets the increment and blames the loop statement
  • Thinks two-argument `iter()` yields the sentinel too
  • Iterates a counter then breaks on a computed condition

context