Python while and for Loops
Objective 2.2 - perform different types of iterations - is the other half of PCEP's 29% control-flow block. Python has two loop statements and they are for different jobs: `while` tests a condition, `for` walks a sequence. Most of the questions are really about `range`, which has three argument forms, produces nothing until you iterate it, and returns an empty sequence rather than an error when the arguments make no sense.
Control Flow Guide 11 of 26 Beginner
- Python3.14.4
- OSUbuntu 26.04 LTS
- pip25.1.1
- TimeAbout 17 min
- Reviewed23 August 2026
Written against the versions above. `range` has been a lazy object rather than a list since Python 3.0 - that is one of the defining Python 3 changes. Material showing `range(3)` printing as `[0, 1, 2]` is Python 2 and should be discarded.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| RUNNER01 | 192.168.0.27 | Ubuntu 26.04 LTS | Python 3.14.4 - the only machine this path needs | 2 Core | 4 GB | 50 GB |
Before you start
-
while: test, run, repeat
A
whileloop tests its condition before every iteration, including the first. If the condition is true it runs the block and tests again.Nothing changes the condition for you. If the body does not move the loop towards falsity, it never ends.
bash Example session cat > ~/py/whilebasic.py <<'PY'n = 0while n < 3: print("n is", n) n += 1print("loop finished with n =", n)PYpython3 ~/py/whilebasic.pyn is 0n is 1n is 2loop finished with n = 3Expected result
n is 0,1,2, thenloop finished with n = 3.Success conditionYou can trace a
whileand say what the counter is when it stops. -
A while whose body never runs
Because the test comes first, a
whilecan execute zero times. There is no do-while in Python - no form that guarantees one pass.bash Example session cat > ~/py/whilenever.py <<'PY'n = 10while n < 3: print("this body never runs")print("condition was false before the first iteration")PYpython3 ~/py/whilenever.pycondition was false before the first iterationExpected resultOnly the message after the loop.
Success conditionYou know a
whilebody is not guaranteed to run at all. -
for: walk something, one item at a time
fordoes not count. It takes each item of an iterable in turn and binds it to the loop variable. Strings, lists, tuples, dictionaries, sets andrangeobjects are all iterable.Four loops below, over four different types.
bash Example session cat > ~/py/forover.py <<'PY'for ch in "abc": print("char:", ch) for item in [10, 20]: print("list item:", item) for item in (1, 2): print("tuple item:", item) for key in {"a": 1, "b": 2}: print("dict key:", key)PYpython3 ~/py/forover.pychar: achar: bchar: clist item: 10list item: 20tuple item: 1tuple item: 2dict key: adict key: bExpected resultThree characters, two list items, two tuple items, then two dictionary keys.
Success conditionYou can loop over any of the four collection types.
-
range, in all three forms
rangeis where most of the loop questions actually live. It takes one, two or three arguments:| Call | Means | |---|---| |
range(stop)| 0 up to but not including stop | |range(start, stop)| start up to but not including stop | |range(start, stop, step)| as above, in steps ofstep, which may be negative |list()is wrapped around each one below only so you can see the contents - the last line shows whatrangereally is.bash Example session cat > ~/py/ranges.py <<'PY'print("range(5) ", list(range(5)))print("range(2, 5) ", list(range(2, 5)))print("range(2, 11, 3) ", list(range(2, 11, 3)))print("range(5, 0, -1) ", list(range(5, 0, -1)))print("range(5, 0) ", list(range(5, 0)))print("range(0) ", list(range(0)))print("range(5) itself ", range(5), type(range(5)).__name__, "len", len(range(5)))PYpython3 ~/py/ranges.pyrange(5) [0, 1, 2, 3, 4]range(2, 5) [2, 3, 4]range(2, 11, 3) [2, 5, 8]range(5, 0, -1) [5, 4, 3, 2, 1]range(5, 0) []range(0) []range(5) itself range(0, 5) range len 5Expected result
[0, 1, 2, 3, 4],[2, 3, 4],[2, 5, 8],[5, 4, 3, 2, 1], then two empty lists, thenrange(0, 5) range len 5.Success conditionYou can produce any sequence the objective asks for, forwards or backwards.
-
The two ways range refuses
Only two argument combinations actually raise, and they raise different exceptions.
bash Example session python3 -c 'print(list(range(1, 10, 0)))'Traceback (most recent call last): File "<string>", line 1, in <module> print(list(range(1, 10, 0))) ~~~~~^^^^^^^^^^ValueError: range() arg 3 must not be zero[exit 1]python3 -c 'print(list(range(1.5)))'Traceback (most recent call last): File "<string>", line 1, in <module> print(list(range(1.5))) ~~~~~^^^^^TypeError: 'float' object cannot be interpreted as an integer[exit 1]Expected result
ValueError: range() arg 3 must not be zero, thenTypeError: 'float' object cannot be interpreted as an integer.Success conditionYou can name both exceptions and say which argument caused each.
-
The loop variable outlives the loop
A
forloop does not create a scope. The loop variable is an ordinary variable in the enclosing scope, and it keeps its last value after the loop ends.If the loop never ran, it was never assigned - and asking for it then does what asking for any unassigned name does.
bash Example session cat > ~/py/loopvar.py <<'PY'for i in range(3): passprint("i is still", i) for j in []: passprint("j is", j)PYpython3 ~/py/loopvar.pyi is still 2Traceback (most recent call last): File "/home/sysadmin/py/loopvar.py", line 7, in <module> print("j is", j) ^NameError: name 'j' is not defined[exit 1]Expected result
i is still 2, then aNameErrorforj.Success conditionYou can say what a loop variable holds after the loop, including when it holds nothing.
-
Nested loops
A loop can contain a loop. The inner one runs completely on every iteration of the outer one, so the total number of body executions is the product.
Three outer iterations, two inner each: six lines of output, arranged into three rows by the
print()at the outer level.bash Example session cat > ~/py/nestedloops.py <<'PY'for i in range(3): for j in range(2): print(i, j, end=" ") print()PYpython3 ~/py/nestedloops.py0 0 0 11 0 1 12 0 2 1Expected resultThree rows, each holding two pairs.
Success conditionYou can count the iterations of a nested loop.
-
What the exam does with this objective
Objective 2.2 covers both loops and their control statements, so it carries most of block 2's 29%. From this half of it:
"How many times does this loop run?" -
rangeexcludes its stop value."What does
range(5, 0)produce?" - an empty sequence, not an error."What is
iafter the loop?" - the last value for afor, one past the last for awhile."What does iterating a dictionary give you?" - the keys.
"What does
range(3)print?" -range(0, 3), not a list."What exception does
range(1.5)raise?" -TypeError.The technique that pays for itself is writing out the loop variable's value for each iteration in the margin. Every off-by-one question collapses once the sequence is written down.
guide 12 finishes block 2.
bash rm -f ~/py/whilebasic.py ~/py/whilenever.py ~/py/forover.py ~/py/ranges.py ~/py/loopvar.py ~/py/nestedloops.py && ls -A ~/pyExpected resultAn empty scratch directory.
Success conditionYou can predict the iteration count and the final variable value for either loop.
Troubleshooting
A loop runs one time fewer than expected.
Why:
range(stop)excludesstop.range(1, 5)is four values, not five.Fix:Use
range(1, n + 1)when you want 1 to n inclusive. Write the sequence out before trusting it.A
whileloop never ends.Why: Nothing in the body changes the condition - usually a missing increment.
Fix:Ctrl-C to stop it. Make sure the body moves the condition towards false on every pass, and prefer
forwith arangewhen the count is known in advance.TypeError: 'float' object cannot be interpreted as an integer.Why: A float passed to
range, often from a division -range(n / 2).Fix:Use floor division:
range(n // 2)./always produces a float, even for exact divisions - see guide 7.for i in range(...)gives aNameErrorforiafter the loop.Why: The loop ran zero times, so
iwas never assigned.Fix:Initialise it before the loop if the code after it needs a value. An empty iterable is not an error and produces no binding.
Looping a dictionary gives keys where values were wanted.
Why: That is what iterating a dictionary does.
Fix:Use
.values()for values or.items()for pairs. See guide 16.