Python10 min readCode verified

Loops: for, while, and Tracing the Nested Ones by Hand

Loops are the first place an exam can ask you to be the computer. There is a reliable way to do that, and it is not reading the code harder. Every result on this page was executed, not remembered.

for, and the three shapes of range#

A for loop walks through the items of something — a list, a string, a range:

fruits = ["apple", "banana", "cherry"]
for x in fruits:
    print(x)
apple
banana
cherry

range manufactures a sequence of integers, and takes one, two or three arguments:

for i in range(4):
    print(i, end=" ")
print()
for i in range(2, 5):
    print(i, end=" ")
print()
for i in range(10, 0, -2):
    print(i, end=" ")
print()
0 1 2 3 
2 3 4 
10 8 6 4 2 
FormMeansProduces
range(4)stop only0, 1, 2, 3
range(2, 5)start, stop2, 3, 4
range(10, 0, -2)start, stop, step10, 8, 6, 4, 2

The stop value is always excluded. One argument starts at zero. A negative step counts down — and note that range(10, 0, -2) stops before 0, so 0 never appears.

Off by one, every time

To loop over 1 through 10 inclusive, you need range(1, 11). The rule is the same one that governs slicing, so learning it once pays twice: go one past the last value you actually want.

The accumulator, and the one-character version that breaks it#

Building up a total across iterations is the most reused loop pattern there is. Set a variable before the loop, add to it inside:

total = 0
for i in range(1, 51):
    total = total + i
print(total)
1275

Change one character — drop the total + — and it still runs, still prints a number, and is wrong:

total = 0
for i in range(1, 5):
    total = i
print(total)
4

That is not a sum, it is the last value the loop happened to see. Overwriting where you meant to accumulate is one of the quietest bugs in a first course, because a plausible-looking number still comes out.

while, and the loop that never ends#

A while loop repeats as long as its condition holds. It needs three things and it is missing one of them that causes trouble:

i = 0
while i < 3:
    print(i)
    i += 1
0
1
2
  1. An initialisation before the loop — i = 0.
  2. A condition that tests it — i < 3.
  3. An update inside the body that moves toward making the condition false — i += 1.

Delete the third and the loop runs forever, printing 0 until you interrupt it. There is no error message, because nothing is wrong with the code — you asked it to repeat while i < 3, and i is still 0. Whenever a program hangs, the update line is the first place to look; and if the update sits inside an if, check that the branch actually runs.

Choosing between them: use for when you know how many repetitions there will be, or you are walking through a collection. Use while when the count depends on something you cannot know in advance — what the user types, when a value converges, when a file runs out.

Sentinels and input validation#

Two while patterns cover most of what a first course asks for. A sentinel loop reads until a designated stop value arrives:

nums = []
val = int(input("Enter a number (-1 to stop): "))
while val != -1:
    nums.append(val)
    val = int(input("Enter a number (-1 to stop): "))
print(nums)

Entering 3, then 5, then −1 leaves nums as [3, 5]. Two details are doing the work: the first read happens before the loop so the condition has something to test, and the second read is the last line of the body so the freshly-read value is what gets tested next. The sentinel itself is never appended, because the condition rejects it before the body runs again.

Input validation is the same shape aimed at the other direction — keep asking until the answer is acceptable:

choice = ""
while choice not in ("CAR", "TRUCK", "SUV", "VAN"):
    choice = input("Car, Truck, SUV or Van? ").upper()
print("You chose", choice)

Typing boat asks again; typing suv is accepted and reported as SUV. Initialising choice to the empty string before the loop is what lets the condition run the first time — and .upper() means you compare against four options instead of every capitalisation of them.

break, continue, and the loop else#

for fruit in ["apple", "banana", "cherry"]:
    if fruit == "banana":
        break
    print(fruit)
apple
for fruit in ["apple", "banana", "cherry"]:
    if fruit == "banana":
        continue
    print(fruit)
apple
cherry

break leaves the loop for good. continue abandons this one iteration and goes straight to the next.

A loop can also carry an else, which is unlike any other else in the language. It runs when the loop finishes normally:

for fruit in ["apple", "banana"]:
    print(fruit)
else:
    print("loop finished")
apple
banana
loop finished

And is skipped entirely when a break got there first:

for fruit in ["apple", "banana"]:
    if fruit == "banana":
        break
    print(fruit)
else:
    print("loop finished")
apple

Read it as "else: no break happened." Its natural use is a search, where the else branch is the not-found case.

Tracing a nested loop by hand#

Exams ask you to predict the output of nested loops because it cannot be faked — you either follow the machine or you guess. Reading the code harder does not work. Writing down a table does.

The method: one column per variable that changes, one row per pass of the inner loop, and never skip a row. Two rules make it reliable: the inner loop runs to completion for every single pass of the outer loop, and a variable declared outside both keeps its value across passes rather than resetting.

outer = 0
inner = 0
while outer < 2:
    for k in range(1, 4):
        inner = inner + k
    outer = outer + 1
print("outer:", outer, "inner:", inner)
outer: 2 inner: 12

The inner for adds 1 + 2 + 3 = 6 each time it runs, and it runs twice, so inner reaches 12 — not 6, because it is never reset between outer passes. And outer ends at 2, not 1: the loop exits only after the value that fails the test has been assigned.

The multiplication version:

total = 0
j = 0
while j < 4:
    k = 0
    while k < 4:
        total += 1
        k += 1
    j += 1
print(total)
16

Four outer passes, four inner passes each, one increment per inner pass. Here k = 0 is reset inside the outer loop — move that line above while j < 4: and the inner loop runs only once in total.

And with output, where the placement of print() decides the shape:

for i in "ABC":
    for j in "124":
        print(i, j, end=", ")
    print()
A 1, A 2, A 4, 
B 1, B 2, B 4, 
C 1, C 2, C 4, 

The bare print() is indented to the outer loop, so it fires once per outer pass and ends each row. Indent it one level further and everything lands on separate lines.

Two more worth knowing#

The loop variable outlives the loop:

for i in range(3):
    pass
print(i)
2

It holds the last value it took. Handy occasionally, and a nasty surprise if you reuse the name.

Removing items from the list you are looping over silently skips things:

nums = [1, 2, 3, 4]
for n in nums:
    if n % 2 == 0:
        nums.remove(n)
print(nums)
[1, 3]

The 4 survived. When 2 was removed, everything after it shifted down a position while the loop's internal index kept advancing — so the value that slid into the vacated slot was never examined. Loop over a copy with for n in nums[:], or build a new list instead.

Where these go wrong#

  • Off-by-one on range. The stop value is excluded. range(1, 10) never produces 10.
  • Overwriting instead of accumulating. total = i where you meant total = total + i. Runs fine, prints a number, wrong.
  • Initialising the accumulator inside the loop. It resets every pass, so you always get the last value.
  • No update in a while body. Infinite loop, no error message.
  • Reading input once before a sentinel loop and never again inside it. Same infinite loop, harder to spot.
  • Expecting a while counter to stop at the limit. It ends one past it — that failing value was still assigned.
  • Resetting an inner counter in the wrong place. Above the outer loop instead of inside it, and the inner loop runs once ever.
  • Mis-indenting print() in a nested loop. One level decides rows versus one long line.
  • Removing from a list while looping over it. Items get skipped, and nothing warns you.

More Python guides

Free study guides for a first Python course — every listing executed before publication.

See the Python course →

Frequently asked questions#

Why does range(1, 10) stop at 9?

Because the stop value is excluded. range(start, stop) produces values from start up to but not including stop. To include 10, write range(1, 11). The same exclusive-stop rule applies to slicing.

When should I use a while loop instead of a for loop?

Use for when you know how many times you will repeat, or you are going through the items of a list or string. Use while when the number of repetitions depends on something you cannot know in advance, such as user input or a calculation converging.

Why is my while loop infinite?

Because nothing in the body changes the value the condition tests, so the condition never becomes false. Every while loop needs an update that moves it toward stopping — and if the update sits inside an if, make sure that branch actually runs.

What is the difference between break and continue?

break leaves the loop entirely and skips any else clause attached to it. continue abandons only the current iteration and jumps to the next one, leaving the loop running.

What does an else on a loop do?

It runs when the loop finishes normally, and is skipped when the loop was ended by break. It is useful for searches: the else branch is the not-found case.

Why does my list skip items when I remove from it in a loop?

Because removing an item shifts everything after it down one position while the loop's internal index keeps advancing, so the item that moved into the vacated slot is never examined. Loop over a copy, or build a new list instead.