The while loop is Python's tool for iteration whose length is not known in advance. It runs its body as long as a boolean condition stays truthy, checking the condition before each pass. Reach for while when the number of iterations depends on work being done inside the loop: polling a service until it responds, consuming a queue until it is empty, retrying an operation until it succeeds, or reading user input until a sentinel value arrives.
Every healthy while loop satisfies three properties: the condition can eventually become false, the body makes visible progress toward that end, and there is a cap on how long the loop can run. Missing any one of these gives you an infinite loop, and the first time you write while True: without a break inside the body, your shell will tell you — Ctrl+C is the standard way out. Writing a small maximum-iteration counter at the top of the loop (for _ in range(1000):) is a cheap way to turn an accidental infinite loop into a bounded bug.
The two most common patterns are the sentinel loop and the polling loop. A sentinel loop reads input until a special value arrives: the user types quit, the file returns an empty string, the queue returns None. A polling loop repeats an action until a condition holds: a web request returns success, a file appears on disk, a counter crosses a threshold. Both patterns use while; they differ only in what counts as "done".
Python's walrus operator := was designed partly for while-loop headers. Consider chunk = f.read(4096); while chunk: ... ; chunk = f.read(4096). With := this becomes while (chunk := f.read(4096)):, reading and checking the value in one line. It is a small win but it removes the duplicated read call that is easy to keep in sync wrongly.
Polling loops should almost always sleep between attempts. Without a time.sleep(seconds) the loop spins at the speed of your CPU, wasting power and making servers angry. A good default is exponential back-off: sleep for 1 s, then 2 s, then 4 s, capped at some sensible maximum. For inputs that must not loop forever, wrap the whole while in a timeout measured with time.monotonic() and raise a clear error if the timeout elapses without success.
Sentinel input and the walrus operator
A sentinel is a value that cannot occur as real data, used to mark the end of a stream. input() typically uses the empty string or the word quit; a file reader uses the empty byte-string; a generator uses StopIteration under the hood. Writing the condition with := keeps the read and the check in the same place.
Polling with time budgets
When waiting for an external condition, decide in advance how long you are willing to wait. deadline = time.monotonic() + 30 at the top and while time.monotonic() < deadline: as the loop header gives you a fixed budget. Raise a clearly-named error when the budget runs out; silence after a timeout is a bug magnet.
These are the tools that combine with while to make a robust loop.
| Tool | Purpose |
|---|---|
whilestatement | Repeats a block as long as the condition stays truthy. |
:=walrus operator | Assign and test a value in the same expression. |
time.sleep()function | Pauses the current thread for a number of seconds. |
time.monotonic()function | Monotonic clock that only increases; ideal for timeouts. |
breakstatement | Exits the nearest loop immediately. |
continuestatement | Jumps to the next iteration without finishing the body. |
iter(callable, sentinel)built-in | Turns a zero-arg callable into an iterator that stops on `sentinel`. |
itertools.count()function | Unbounded counter; handy inside a while-with-break. |
Looping Based on Conditions code example
The example reads numbers from a queue with a sentinel, then polls a fake service with a timeout.
# Lesson: Looping Based on Conditions
# Goal: demonstrate sentinel-based and polling-based while loops.
import random
import time
from collections import deque
def consume(queue: deque) -> int:
'''Pop items off the queue until the sentinel None arrives.'''
processed = 0
while queue:
item = queue.popleft()
if item is None: # sentinel value marks "end of data"
break
processed += item
return processed
def wait_for(condition, timeout: float = 5.0, poll: float = 0.1) -> bool:
'''Return True once `condition()` is truthy, or False after `timeout` seconds.'''
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if condition():
return True
time.sleep(poll)
return False
# --- main script ---------------------------------------------------------
queue = deque([1, 2, 3, None, 99]) # 99 will be ignored after the sentinel
print("sum =", consume(queue))
# fake service that succeeds with growing probability
attempts = 0
def flaky_service() -> bool:
global attempts
attempts += 1
return random.random() < attempts / 8 # gets luckier each call
random.seed(0)
ok = wait_for(flaky_service, timeout=1.0, poll=0.01)
print(f"service responded after {attempts} attempts: {ok}")
# walrus-powered sentinel loop reading until the user types 'q'
# (commented so the script runs unattended)
# while (answer := input('next? ')) != 'q':
# print(f'you said: {answer!r}')
Three loops, three purposes:
1) consume() exits either when the queue is empty OR on the sentinel.
2) wait_for() uses time.monotonic() so it is robust to clock changes.
3) time.sleep() prevents the poll loop from burning the CPU.
4) The commented walrus loop is the canonical sentinel-input shape.
Two snippets that replace hand-written counters with cleaner forms.
# Example A: iter(callable, sentinel) is an even cleaner sentinel loop
with open(__file__) as f:
for chunk in iter(lambda: f.read(64), ""):
# chunk is a 64-char slice; the loop stops when read() returns ""
pass # process chunk
print("done reading this file")
# Example B: bounded retry with exponential back-off
import random
def unstable():
return random.random() < 0.2
delay = 0.01
for attempt in range(5):
if unstable():
print(f"succeeded on attempt {attempt + 1}")
break
time.sleep(delay)
delay *= 2
else:
print("gave up")
Verifying the two helpers behave correctly.
assert consume(deque([1, 2, 3])) == 6
assert consume(deque([])) == 0
assert consume(deque([5, None, 10])) == 5 # nothing after sentinel
counter = {"v": 0}
def ready():
counter["v"] += 1
return counter["v"] >= 3
assert wait_for(ready, timeout=1.0, poll=0.0) is True
A sample run (random behaviour depends on the seed):
sum = 6
service responded after 3 attempts: True
done reading this file