← Back to Core Python
Lesson 13 · Operators and Control Flow

while Loops

Conditions, counters, input loops, and avoiding infinite ones.

Beginner30 min

What you will be able to do

  • Write a while loop with a condition that eventually becomes false
  • Name the three parts every counting loop has, and spot a missing one
  • Choose between for and while for a given problem
  • Use while True with break for menus and input loops
  • Cap retries and polling so a loop cannot run forever
  • Recognise the four ways a while loop becomes infinite by accident

The idea, in plain English

A for loop runs once per item in something you already have. A while loop runs as long as a condition holds, and nothing tells it in advance how many times that will be. That difference is the entire reason both exist.

The condition is tested before every iteration, including the first - so a while loop whose condition starts false never runs at all. When the condition becomes false, the loop ends and execution continues below it.

Every counting while loop has three parts, and they are worth naming because forgetting one is the classic bug: initialise the variable before the loop, test it in the condition, and update it inside the body. Miss the update and the condition never changes.

The genuine use for while is when the number of iterations is unknowable in advance: asking until the answer is valid, retrying until something succeeds, polling until a job finishes, draining a queue that other code is still filling. If you know the count, a for loop says so more clearly and cannot hang.

Worked example: Retrying a submission three times, then giving up cleanly.

Initialise, test, update

Three parts, and the loop only terminates if all three are present and agree with each other. The variable that appears in the condition must be the one that changes in the body.

The question to ask of any while loop you write is: what, exactly, will eventually make this condition false? If you cannot point at the line that does it, the loop will not stop.

This is also why a for loop cannot hang the same way - the iterator does the updating for you, so there is nothing to forget.

Four ways a while loop runs forever
No update at allwhile count <= 5 with no count += 1 in the body. The condition is frozen.
Updating the wrong nameThe condition tests count but the body increments index. Both look busy; neither helps.
continue above the updatecontinue jumps back to the condition, skipping the increment below it. Covered properly in lesson 14.
while True with no breakDeliberate syntax, accidental result. Every while True needs a visible exit.
A condition that cannot be reachedwhile balance != 0 on a float, where the value skips past zero. Prefer <= or >=.

Tip: If you do hang a terminal, Ctrl+C stops it. Python raises KeyboardInterrupt, which ends the loop and the program.

for or while?

The rule of thumb: for iterates over items, while waits for a condition. If you can name the collection or say how many times up front, use for.

while earns its place when the end is genuinely unknown at the start - user input that may be wrong any number of times, an operation that may need retrying, a job that finishes when it finishes, a queue other code is still adding to.

Counting with while is legal and almost always the wrong choice. `while i < len(items)` with manual indexing is a for loop with three extra chances to get it wrong, and it hides what the code is doing.

Which loop
Every item in a listfor. The iterator handles position and termination.
A fixed number of timesfor with range(). The count is visible in the header.
Until the input is validwhile. You cannot know how many attempts it takes.
Retry until success or limitwhile, with both conditions joined by and.
Poll until a job completeswhile, with a maximum attempt count.
Drain a queuewhile queue - the loop ends when the collection empties.
Menu until the user quitswhile True with a break on the exit option.

Cap everything that retries

`while not success:` is an outage waiting to happen. If the thing being retried is down, the loop spins forever, and in a request handler it takes a thread with it.

Two conditions joined by and is the shape: `while attempts < max_attempts and not success:`. When the loop ends, check which condition ended it - success or exhaustion - because those need different responses.

Note the difference between and and or here. `while attempts < 3 and not success` stops on either exhaustion or success. `while attempts < 3 or not success` keeps going while either holds, so it will not stop on exhaustion alone - which is almost never what was meant.

A real retry also waits between attempts, and usually waits longer each time. Retrying a struggling service immediately is how a slowdown becomes an outage - which is the circuit breaker story from the architecture course, arriving here as a loop.

Watch out: Polling needs the same cap. A loop that checks a job status until it is done should also stop after N attempts and report a timeout.

while True, and the walrus alternative

Sometimes the exit condition can only be evaluated part-way through the body - you have to read the input before you can tell whether it means stop. Writing that as a real condition forces you to duplicate the read: once before the loop to prime it, and again at the bottom.

while True with a break avoids the duplication. The cost is that the exit is no longer visible in the header, so the break should be near the top of the body and obvious.

Since Python 3.8 there is a third option: the walrus operator assigns and tests in one step, so `while (line := f.readline()):` reads and checks together. It puts the exit condition back in the header without priming.

Syntax and examples

The three parts
count = 1 # initialise while count <= 5: # test - before every iteration, including the first print(count) count += 1 # update - the line that ends the loop # 1 2 3 4 5 # The condition is tested first, so a false start means zero iterations count = 10 while count < 5: print("never runs") # < and <= are not the same boundary count = 1 while count < 5: count += 1 print(count) # 5 - stopped as soon as it reached 5
Counting down, and counting backwards
seconds = 5 while seconds > 0: print(seconds) seconds -= 1 print("Time's up") # 5 4 3 2 1 Time's up # A boolean can drive the loop directly running = True ticks = 0 while running: ticks += 1 if ticks == 3: running = False print(ticks) # 3
Input until it is valid
# The classic use: you cannot know how many attempts it will take password = "" while password != "python123": password = input("Enter password: ") print("Access granted") # Normalising in the condition handles EXIT, Exit and " exit " alike command = "" while command.strip().lower() != "exit": command = input("Enter command: ") print("Goodbye")
while True plus break
# When the exit condition is only knowable mid-body while True: command = input("Enter command: ").strip().lower() if command == "exit": break # the exit, near the top and obvious print("You entered:", command) # The same shape drives an interactive menu while True: print("\n1. View courses 2. View profile 3. Exit") choice = input("Choose: ").strip() if choice == "1": print("Showing courses") elif choice == "2": print("Showing profile") elif choice == "3": print("Goodbye") break else: print("Invalid option")
Retry, capped
import time max_attempts = 3 attempt = 0 submitted = False while attempt < max_attempts and not submitted: attempt += 1 print(f"Submitting - attempt {attempt}") submitted = attempt == 2 # pretend the second one works if not submitted and attempt < max_attempts: # Wait longer each time. Retrying instantly is how a slow # service becomes a dead one. time.sleep(2 ** attempt) # Check WHICH condition ended the loop - they mean different things if submitted: print("Submitted") else: print(f"Gave up after {attempt} attempts")
Polling with a timeout
statuses = ["processing", "processing", "processing", "completed"] attempts = 0 max_attempts = 5 job_status = "processing" while job_status != "completed" and attempts < max_attempts: job_status = statuses[attempts] # stands in for an API call attempts += 1 print(f"Attempt {attempts}: {job_status}") if job_status == "completed": print("Job finished") else: print("Timed out waiting for the job") # Attempt 1: processing # Attempt 2: processing # Attempt 3: processing # Attempt 4: completed # Job finished
Draining a queue
jobs = ["job1", "job2", "job3"] # The list is the condition - an empty list is falsy, so the loop ends while jobs: job = jobs.pop(0) print("Processing", job) print(jobs) # [] # pop(0) shifts every remaining element, so a long queue is O(n^2). # A deque pops from the left in constant time. from collections import deque queue = deque(["job1", "job2", "job3"]) while queue: print("Processing", queue.popleft()) # A while loop suits a queue precisely because it can grow while you work tasks = deque(["a"]) while tasks: task = tasks.popleft() if task == "a": tasks.extend(["b", "c"]) # adding to a list you are for-looping is a bug print(task) # a b c
while/else, and the walrus
# else runs when the loop ends normally - that is, without a break numbers = [10, 20, 40] index = 0 while index < len(numbers): if numbers[index] == 30: print("Found") break index += 1 else: print("Not found") # runs, because nothing broke # Priming a loop just to test its first value is a smell... line = input("> ") while line != "quit": print(line) line = input("> ") # the read is written twice # ...and the walrus removes the duplication while (line := input("> ")) != "quit": print(line)

Watch out: pop(0) on a list moves every remaining element, so draining a long queue that way is quadratic. collections.deque.popleft() is constant time.

The shapes worth knowing

Counter

Initialise, test, update. Use a for loop instead unless the step is irregular.

while n <= 5:\n    n += 1
Sentinel

Loop until a particular value arrives. The condition names the stopping value.

while cmd != "exit":
while True + break

For when the exit is only knowable mid-body. Put the break near the top.

while True:\n    if done: break
Capped retry

Two conditions with and, so it stops on success or on exhaustion.

while n < 3 and not ok:
Polling

A capped retry that checks external state, with a timeout message.

while status != "done" and n < 5:
Queue drain

The collection is the condition; empty is falsy. Works while the queue grows.

while jobs:\n    jobs.pop(0)
Walrus

Assign and test in one step, so the read is not duplicated. Python 3.8+.

while (x := read()):

for or while, by situation

A list, string, or dict

for - the iterator handles position and termination.

A known number of times

for with range(), so the count is visible in the header.

Input until valid

while - the number of attempts is unknowable.

Retry until success or limit

while, with both conditions joined by and.

Poll external state

while, capped, with a wait between checks.

A queue that grows as you work

while - a for loop cannot cope with the collection changing.

A menu or CLI

while True with a break on the exit option.

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

Boundary check

“n=1 while n < 5: n += 1 print(n)”

Condition false at the start

“n=10 while n < 5: print("never") print("done")”

Queue drains itself

“q=["a","b"] while q: print(q.pop(0)) print(q)”

else means no break

“n=0 while n<3: n+=1 else: print("finished", n)”

What usually goes wrong

Forgetting the update

The commonest infinite loop there is. Nothing in the body changes the variable in the condition, so it stays true forever. Ask of every while loop: which line makes this false?

✗ count = 1
while count <= 5:
    print(count)
✓ count = 1
while count <= 5:
    print(count)
    count += 1
Updating a different variable

The body looks busy, so the bug is harder to see than a missing update. The name in the condition and the name being changed have to be the same one.

✗ while count <= 5:
    print(count)
    index += 1
✓ while count <= 5:
    print(count)
    count += 1
while True with no way out

Deliberate syntax, accidental result. Every while True needs a break that a reader can find, and preferably near the top of the body.

✗ while True:
    process()
✓ while True:
    item = get()
    if item is None:
        break
    process(item)
Retrying without a limit

If the thing being retried is down, the loop spins forever - and inside a request handler it holds a thread while doing so. Cap it, and wait longer between attempts.

✗ while not success:
    success = call_api()
✓ while attempts < 3 and not success:
    attempts += 1
    success = call_api()
Joining retry conditions with or

and stops when either condition fails. or keeps going while either holds, so exhausting the attempts does not end the loop on its own. Reason about which you mean.

✗ while attempts < 3 or not success:
✓ while attempts < 3 and not success:
Testing a float for exact equality

A value stepped by a fraction can skip straight past the target, and the condition never becomes false. Use a bound rather than equality.

✗ while balance != 0:
    balance -= 0.1
✓ while balance > 0:
    balance -= 0.1
Counting with while when for would do

Manual indexing gives you three things to get wrong - the start, the bound, and the increment - and hides that you are simply visiting every item.

✗ i = 0
while i < len(items):
    print(items[i])
    i += 1
✓ for item in items:
    print(item)

Best practices

  • Use for when you know the collection or the count; keep while for genuinely open-ended repetition.
  • Name the line that ends the loop before you write the body.
  • Cap every retry and every poll, and wait longer between attempts.
  • After a capped loop, check which condition ended it - success and exhaustion need different handling.
  • Put the break of a while True near the top of the body so the exit is visible.
  • Use `while queue:` rather than `while len(queue) > 0:`.
  • Use a deque when you are popping from the left.
  • Keep the body small - if it does validation, I/O and business logic, extract functions.

Practice

Write these yourself before opening anything. Getting them wrong first is most of how this sticks.

1.

Use a while loop to print 1 to 10.

Show solution
count = 1 while count <= 10: print(count) count += 1
2.

Count down from 10 to 1, then print "Blast off!".

Show solution
count = 10 while count > 0: print(count) count -= 1 print("Blast off!")
3.

Add the numbers 1 to 10 with a while loop and print the total. It should be 55.

Show hint

Two variables: one to count, one to accumulate.

Show solution
number = 1 total = 0 while number <= 10: total += number number += 1 print(total) # 55
4.

Print every even number from 1 to 20 using a while loop.

Show hint

Either test with % 2, or step by 2 - the second is fewer iterations.

Show solution
number = 2 while number <= 20: print(number) number += 2 # stepping by 2 beats testing every number
5.

Keep asking for a password until "python123" is entered, but allow only three attempts. Print "Maximum attempts reached" if they run out.

Show hint

Two conditions joined by and, then check which one ended the loop.

Show solution
correct = "python123" attempts = 0 max_attempts = 3 entered = "" while attempts < max_attempts and entered != correct: entered = input("Password: ") attempts += 1 if entered == correct: print("Access granted") else: print("Maximum attempts reached")
6.

Drain the queue ["job1", "job2", "job3"], printing each job, and show the list is empty afterwards.

Show solution
jobs = ["job1", "job2", "job3"] while jobs: print("Processing", jobs.pop(0)) print(jobs) # []
7.

Search [10, 20, 40] for 30 with a while loop, printing "Found" or "Not found" - without using a flag variable.

Show hint

while/else. The else runs only when no break happened.

Show solution
numbers = [10, 20, 40] index = 0 while index < len(numbers): if numbers[index] == 30: print("Found") break index += 1 else: print("Not found")
Coding challenge

Number guessing game, with attempts

The classic while-loop exercise, done properly: the player gets a limited number of guesses, is told whether each is too high or too low, and the program distinguishes winning from running out.

It should
  • The secret number is 7 and the player gets three attempts
  • After each guess, say "Too low", "Too high", or "Correct!"
  • Stop immediately on a correct guess
  • Print "Game over" only when the attempts run out, never after a win
  • Handle input that is not a number without crashing
  • Use a while/else, or check afterwards which condition ended the loop
Start here
secret = 7 max_attempts = 3 attempt = 0 # Use this instead of input() to test without typing guesses = iter(["5", "nine", "9", "7"]) def ask(prompt): value = next(guesses) print(prompt + value) return value
Show one solution
One solution
secret = 7 max_attempts = 3 attempt = 0 won = False guesses = iter(["5", "nine", "9", "7"]) def ask(prompt): value = next(guesses) print(prompt + value) return value while attempt < max_attempts and not won: raw = ask(f"Attempt {attempt + 1} - guess: ") # A bad value should not cost an attempt, so check before counting it if not raw.strip().lstrip("-").isdigit(): print(" Please enter a whole number") continue attempt += 1 guess = int(raw) if guess < secret: print(" Too low") elif guess > secret: print(" Too high") else: print(" Correct!") won = True # Which condition ended the loop decides the message if not won: print(f"Game over - the number was {secret}") # Attempt 1 - guess: 5 # Too low # Attempt 2 - guess: nine # Please enter a whole number # Attempt 2 - guess: 9 # Too high # Attempt 3 - guess: 7 # Correct!

Key points

  • A while loop repeats as long as its condition is true, and the condition is tested before every iteration.
  • If the condition starts false, the body never runs at all.
  • Every counting loop has three parts: initialise, test, update. Missing the update is the classic infinite loop.
  • The variable in the condition must be the one the body changes.
  • for iterates over items; while waits for a condition. If you know the count, use for.
  • while True with break suits exits that are only knowable mid-body - put the break near the top.
  • Cap every retry and every poll, and check afterwards which condition ended the loop.
  • Join retry conditions with and, not or.
  • `while queue:` uses truthiness and ends when the collection empties.
  • Use deque.popleft() rather than list.pop(0) for anything but a short queue.
  • A while/else runs the else only when the loop ends without a break.
  • The walrus operator lets you assign and test together, removing the primed-read duplication.

Quick check before you move on

What does this print? count = 1 while count < 5: count += 1 print(count)
5. The loop stops as soon as count reaches 5, because the condition is tested before each iteration - so the final value is the first one that failed the test.
What happens when the condition is already false on the first test?
The body never runs at all. A while loop can execute zero times.
Why does `while jobs:` eventually stop when the body pops from jobs?
Because an empty list is falsy. When the last item is removed, the condition evaluates to False and the loop ends.
Why is `while attempts < 3 or not success:` wrong for a retry?
or keeps looping while either condition holds, so running out of attempts does not end it on its own - only success does. and is what stops it on either.

Interview questions

How would you design a safe polling loop?

Cap the attempts or use a deadline based on wall-clock time, wait between checks with an increasing delay plus a little jitter so many clients do not align, and treat "gave up" as a distinct outcome from "finished" rather than falling through to the same code. Log each attempt so a timeout is diagnosable. And prefer being told over asking - a webhook or a queue message beats polling entirely when it is available.

What is the difference between count-controlled and condition-controlled loops?

A count-controlled loop knows its number of iterations before it starts, which is what for expresses - the iterator supplies the items and the termination. A condition-controlled loop runs until some state changes, which is what while expresses, and the responsibility for making that state change is yours. Almost every infinite loop is a condition-controlled loop where nobody wrote the line that ends it.

Why can a long-running while loop be a problem in a server?

Because it holds whatever is running it. In a synchronous handler it occupies a worker thread that cannot serve anyone else, and a loop that sleeps between polls blocks for the whole duration. In async code, a CPU-bound while loop with no await blocks the entire event loop, not just that one request. The answers are a background worker, an async sleep that yields, or not polling at all.

When would you use the walrus operator in a while loop?

When the condition depends on a value you have to compute or read first. Without it you prime the loop with a read before it and repeat the read at the bottom, which is two places to change and a real source of bugs. `while (chunk := f.read(8192)):` reads and tests in one step and keeps the exit condition in the header where it belongs.

How do you avoid a retry storm?

Cap the attempts, back off exponentially rather than retrying immediately, and add jitter so clients that failed together do not all return together. Only retry errors that might succeed on a second attempt - a timeout or a 503, never a 400. And at scale, put a circuit breaker in front: once enough attempts have failed, stop trying for a while instead of every client discovering the outage individually.

Is `while len(items) > 0:` worse than `while items:`?

Functionally identical for a list, and less idiomatic. `while items:` uses truthiness, reads as a sentence, and works for anything that defines __len__ or __bool__. The explicit form is only better when the reader might not know the object is a collection, or when zero is genuinely different from empty for that type.

Quiz

  1. 1.

    What is a while loop, and when should you use one?

  2. 2.

    What happens if the condition is false the first time it is tested?

  3. 3.

    What causes an infinite loop?

  4. 4.

    When is while True appropriate?

  5. 5.

    When does the else block of a while loop run?

  6. 6.

    Why should a retry loop have a maximum?

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...