Abstract¶
Iteration is a useful construct that specifies how certain code should be executed repeatedly, thereby avoiding the need for code duplication for repetitive tasks. By leveraging iteration, programmers can streamline their code, making it easier to read and maintain. Readers will learn to write iterations using the while statement, and how unintended infinite loops can be introduced without careful choices of looping/termination conditions. For definitive loops where the number of iterations is known before runtime, readers will learn to write for statements to repeatedly execute a block of code with target variables ranging over some iterable collections of items.
import math
from ipywidgets import interact
%reload_ext divewidgetsMotivation¶
An important application of programming is to automate the boring stuff:
A significant contributor to boredom is often the repetitive nature of the task. For instance:
Calculating the maximum value of a sequence of numbers, which can be indefinitely long.
Continuously prompting users for input until it meets the validation criteria.
...
%%hermes
List three very common repetitive tasks that can be best solved by using
iteration in programming. Do not include any code.🌿 Fork/new: main (will create on click)
See Also
Here are three very common repetitive tasks that iteration excels at solving:
Processing every item in a collection — For example, calculating the total of a list of prices, finding the largest value in a set of scores, or transforming each element (like converting all names to uppercase). A loop lets you apply the same operation to every item without writing the operation separately for each one.
Repeating an action until a condition is met — For example, asking a user to re-enter input until it’s valid, or polling a server until a response arrives. A loop keeps trying and checks the stopping condition each time, so the program adapts to whatever it takes to succeed.
Performing a fixed number of repetitions — For example, running a simulation 1,000 times to gather statistics, printing a table row by row, or executing a warm-up routine before a benchmark. A loop handles the counting and repetition automatically instead of copying the same block of code over and over.
num = int(input(">"))
if 1 <= num:
print(1)
if 2 <= num:
print(2)
if 3 <= num:
print(3)
### BEGIN SOLUTION
import sys
_ = sys.getrecursionlimit()
sys.setrecursionlimit(max(num+50, _))
def loop_to(n):
print(n)
if n<num:
loop_to(n+1)
loop_to(4)
sys.setrecursionlimit(_)
# Alternative solution 1:
# i = 4
# while i <= num:
# print(i)
# i += 1
# Alternative solution 2:
# for i in range(4, num+1):
# print(i)
### END SOLUTION> 10
1
2
3
4
5
6
7
8
9
10
Should I reuse code by copy-and-paste?
Code duplication is a bad practice:
Duplicate code is hard to read/write/maintain. Imagine what you need to do to change some code.
The number of repetitions may not be known before runtime, as in Exercise 1.
Instead, a programmer should write a loop/iteration that specifies how a piece of code should be executed repeatedly.
One way to reuse code is to write iterations/loops.[1]
%%hermes
In one paragraph, give a concrete example to explain whether code duplication
is a good programming practice for carrying out repetitive tasks, and how
iteration may avoid code duplication.🌿 Fork/new: main (will create on click)
See Also
Code duplication is considered a poor programming practice because repeating the same logic in multiple places makes code harder to read, maintain, and debug — if a bug is found or a change is needed, every copy must be updated, increasing the risk of introducing inconsistencies or missing a spot. Iteration avoids this problem entirely by expressing the repeated behaviour once inside a loop: the loop body contains the logic a single time, while the loop mechanism handles repeating it as many times as needed. For example, instead of writing the same calculation for each student’s grade by copy-pasting lines, a loop iterates over the list of students and applies the calculation once to each — so the logic lives in exactly one place, and adding more students requires no code changes at all.
%%hermes
Code duplication appears more flexible than iteration since each copy
can be tweaked independently without touching a shared loop.🌿 Fork/new: main (will create on click)
See Also
That argument sounds plausible at first, but the “flexibility” it describes is actually a liability in disguise. When each copy can be tweaked independently, it’s easy for copies to drift apart unintentionally — one copy gets a fix while another doesn’t, creating subtle bugs that are hard to track down. The very independence that feels like flexibility becomes code that is inconsistent, fragile, and expensive to maintain. If different behaviour is genuinely needed, the correct solution is not to duplicate code but to parameterise it: a single loop with a variable or function that changes its behaviour based on the data it processes. That way you get both variety and a single source of truth — real flexibility without the maintenance cost of duplication.
Loops¶
While Loop¶
We can use the while statement to repeatedly execute certain code until a specified looping condition is false. A simplified version of the syntax is:
while_stmt ::= "while" assignment_expression ":" suite%%flowchart
st=>start: Start
cond1=>condition: assignment_expression
suite1=>operation: suite
e=>end
st(right)->cond1
cond1(yes, right)->suite1(right)->cond1
cond1(no)->eIn other words, suite is repeatedly executed as long as assignment_expression is interpreted as True by the control flow statement.
As an example, the following program keeps asking for user input until the input is non-empty.
while not input("Input something please:"):
passIf user inputs nothing, input returns an empty string '', which is regarded as False by control flow statements, and so the looping condition not input('...') is True.
Consider the following while loop which attempts to solve Exercise 1:
num = int(input(">"))
i = -1
while i != num:
i += 1
print(i)> 10
0
1
2
3
4
5
6
7
8
9
10
Solution to Exercise 2
When the input corresponds to an integer , the while loop becomes an infinite loop. The following is a formal proof by induction of a loop invariant:
Assume as an inductive hypothesis that i > num at the beginning of each iteration, and so the looping condition is always true. The base case i == -1 holds trivially since num <= -2 by the assumption that user inputs an integer . To prove the induction, note i is incremented by 1 for each iteration, so it remains larger than num at the beginning of the next iteration.
When writing loops, it’s essential to avoid creating unintended infinite loops, which can bring your server to a grinding halt.
Indeed, it is impossible to automatically determine whether a program terminates, a challenge known as the Halting problem.
The following video gives an intuitive explanation of the halting problem and why no program can automatically determine whether a given program terminates.
The Halting Problem
%%hermes
Prove that it is impossible to automatically determine whether a program terminates.🌿 Fork/new: main (will create on click)
See Also
This is Turing’s Halting Problem — proved undecidable in 1936. Here’s the proof by contradiction:
Step 1: Assume the opposite. Suppose there exists a program halts(P, I) that takes two inputs — a program P and an input I — and correctly returns True if P(I) terminates and False if P(I) runs forever.
Step 2: Build a paradoxical program. Using our hypothetical halts, construct:
def paradox(P):
if halts(P, P):
loop_forever()
else:
return # terminate immediatelyThis program takes another program P, feeds it its own code as input (P(P)), and does the opposite of what halts predicts.
Step 3: Feed it to itself. What happens when we run paradox(paradox)?
Case A: If
halts(paradox, paradox)returnsTrue(predicts termination), thenparadox(paradox)enters an infinite loop — so it doesn’t terminate. Contradiction.Case B: If
halts(paradox, paradox)returnsFalse(predicts non-termination), thenparadox(paradox)returns immediately — so it does terminate. Contradiction.
Conclusion: Both outcomes contradict the prediction. Therefore, halts(P, I) cannot exist. No algorithm can solve the halting problem in the general case.
The core trick is self-reference — the program receives its own description as input, creating a logical loop analogous to the liar paradox (“This statement is false”). Turing showed this isn’t a limitation of current technology but a fundamental boundary of computation itself.
As a hint, consider borrowing some ideas from a similar proof by contradication below:
%%hermes
Explain why real numbers are uncountable.
In particular, explain concisely in two paragraphs using the diagonalization
argument.🌿 Fork/new: main (will create on click)
See Also
Cantor’s diagonalization argument proves the real numbers are uncountable by contradiction. Suppose the real numbers between 0 and 1 can be listed in a sequence, one for each natural number — meaning they are countable. Each number has an infinite decimal expansion, so the list looks like a table where the n-th row is the n-th number and the columns are its decimal digits. Now construct a new number by reading the diagonal: take the first digit of the first number, the second digit of the second number, and so on, and change each digit (for instance, add 1 modulo 10). The resulting number differs from the first number in its first digit, from the second number in its second digit, and in general from the n-th number in its n-th digit.
Because this new number differs from every number on the list in at least one position, it cannot appear anywhere in the list — yet it is a valid real number between 0 and 1. This contradicts the assumption that every such real number was already listed, so no such complete list can exist. Therefore the real numbers are strictly more numerous than the natural numbers: they are uncountable.
For Loop¶
One way to fix the issue in Exercise 2 is to modify the condition of the while loop. (How?) However, we will learn a better fix as follows using the for statement:
for i in range(int(input(">")) + 1):
print(i)> 10
0
1
2
3
4
5
6
7
8
9
10
A simplified version of the syntax is:
for_stmt ::= "for" target_list "in" starred_list ":" suiteA for loop is written as for <variable> in <iterable>: followed by an indented block; each iteration binds <variable> to the next element of the iterable (such as a list, tuple, range, or generator) and executes the indented statements under the colon, so the syntax is typically for i in range(5): on one line, then a consistently indented block (e.g., four spaces) for the body, as in for i in range(5): print(i), where print(i) runs with i taking the values 0, 1, 2, 3, 4.
How to print from 0 to 4?
Let us visualize the execution of a simpler version of the for loop that prints from 1 up to 4:
%%pytutor
for i in 1, 2, 3, 4:
print(i)iis automatically assigned to each element in the sequence1, 2, 3, 4one-by-one from left to right.[2]After each assignment, the body
print(i)is executed.
Unlike other languages such as C, Python’s for loop uses the in keyword to
iterate over a collection of objects called an
iterable.
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 4; ++i) {
printf("%d\n", i);
}
return 0;
}One benefit of using iterables is that it is difficult to write an infinite
loop using the Python for loop.[3] Another reason is that
this allows Python to be more expressive and easy to read. For instance:
tuples = (0, "l"), (1, "o"), (2, "o"), (3, "p")
for i, c in tuples:
print(i, c)0 l
1 o
2 o
3 p
for i, c in enumerate("loop"):
print(i, c)0 l
1 o
2 o
3 p
How to print up to a user-specified number?
The complete fix to Exercise 2 uses range:
stop = int(input(">")) + 1
for i in range(stop):
print(i)Why add 1 to the user input number?
range(stop) generates a sequence of integers from 0 up to but excluding stop:
1 2 3 4 5 6 7 8 9 10Init signature: range(self, /, *args, **kwargs) Docstring: range(stop) -> range object range(start, stop[, step]) -> range object Return an object that produces a sequence of integers from start (inclusive) to stop (exclusive) by step. range(i, j) produces i, i+1, i+2, ..., j-1. start defaults to 0, and stop is omitted! range(4) produces 0, 1, 2, 3. These are exactly the valid indices for a list of 4 elements. When step is given, it specifies the increment (or decrement).
How to start from a number different from 0?
We can use two arguments to specify the start and stop.
for i in range(1, 5):
print(i)1
2
3
4
What about a step size different from 1?
We can use an additional step argument.
for i in range(0, 5, 2):
print(i) # starting number must also be specified. Why?0
2
4
Ask an AI agent to write a for loop that processes a list of student
scores (e.g., [85, 42, 93, 67, 55, 78, 30, 91]) and categorizes each
score into grade bands: A (90+), B (80-89), C (70-79), or F (below 70).
Then review whether the loop is more Pythonic and readable than a
while loop would have been for this task.
Solution to Exercise 3 #
A good AI-generated solution iterates over the scores list and uses
if-elif-else to categorize each score. Key evaluation points:
The loop correctly iterates over all scores using
for score in scores:.Conditions are in the correct order (e.g.,
if score >= 90:beforeelif score >= 80:) so lower bounds are not missed.Edge cases like
90,80,70are handled correctly.A
forloop is more Pythonic here because the number of iterations is known and theforloop directly expresses “process each element.”
How to count down from 4 to 0? Try doing it without addition or subtraction.
### BEGIN SOLUTION
for i in range(4, -1, -1):
print(i)
### END SOLUTION4
3
2
1
0
Print from 0 to a user-specified number but in steps of 0.5.
E.g., if the user inputs 2, the program should print:
0.0
0.5
1.0
1.5
2.0Caution
range only accepts integer arguments, so range(0, 2, 0.5) is invalid because 0.5 is not an integer.
num = int(input(">"))
### BEGIN SOLUTION
for i in range(0, 2 * num + 1, 1):
print(i / 2)
### END SOLUTION> 10
0.0
0.5
1.0
1.5
2.0
2.5
3.0
3.5
4.0
4.5
5.0
5.5
6.0
6.5
7.0
7.5
8.0
8.5
9.0
9.5
10.0
How to print the character '*' repeatedly for m rows and n columns?
Hint
Try using a nested for loop, i.e., a for loop (inner loop) inside the body of another for loop (outer loop).
@interact(m=(0, 10), n=(0, 10))
def draw_rectangle(m=5, n=5):
### BEGIN SOLUTION
for i in range(m):
for j in range(n):
print("*", end="")
print()
# Alternative solution: print(m * ("*" * n + "\n"))
### END SOLUTIONWhat about iterating over characters of a string?
%%pytutor
for character in "loop":
print(character)In Python, str is also regarded as an iterable, or more specifically, a sequence type, namely, a sequence of characters.
The function
lencan return the length of a string.The indexing operator
[]can return the character of a string at a specified location.
message = "loop"
print("length:", len(message))
print("characters:", message[0], message[1], message[2], message[3])
# Negative indexing also allowed.
print("characters:", message[-4], message[-3], message[-2], message[-1])length: 4
characters: l o o p
characters: l o o p
We can also iterate over a string as follows although it is less elegant:
for i in range(len("loop")):
print("loop"[i])l
o
o
p
Print a string assigned to message in one line but in reverse. E.g., 'loop' should be printed as 'pool'.
@interact(message="loop")
def reverse_print(message):
### BEGIN SOLUTION
for i in range(len(message)):
print(message[-i - 1], end="")
print("") # final line break
# Alternative solution
# print(message[::-1])
### END SOLUTIONWhile Loop vs For Loop¶
How to decide whether to use while loop or for loop?
foris often used for a definite loop which has a definite number of iterations before execution.whileis often used for an indefinite loop where the number of iterations is unknown before execution.
It is always possible to replace a for loop by a while loop. Indeed, a for loop such as
for i in range(5): print(i)translates to the following code using a while loop:
%%pytutor
iterable = range(5)
iterator = iter(iterable)
try:
while True:
i = next(iterator)
print(i)
except StopIteration: passWhen executing the for loop,
We can compare the speed of the for loop and while loop using the cell magic %%timeit as follows:
%%timeit
for i in range(5): cur_count = i99.3 ns ± 0.128 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
%%timeit
i = 0
while i < 5:
cur_count = i
i += 1121 ns ± 0.0648 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
%%hermes
Is it always possible to replace a while loop by a for loop?🌿 Fork/new: main (will create on click)
Be careful of AI hallucination.
Ask an AI agent to implement a primality test using while loops that
returns True if a number is prime and False otherwise. Test the
result with edge cases: 0, 1, 2, negative numbers, and large primes.
Discuss whether a for loop approach would be simpler and more
Pythonic.
Solution to Exercise 8 #
A correct primality test using while loops checks divisibility from 2
up to sqrt(n). Evaluation points:
Returns
Falsefor 0 and 1 (not prime by definition).Returns
Truefor 2 (smallest prime).Returns
Falsefor negative numbers.For
n > 2, checks divisibility by2, then by odd numbers up toint(n**0.5) + 1using awhileloop.A
forloop withrange(3, int(n**0.5) + 1, 2)would be simpler and more Pythonic because the iteration bounds are known in advance.
Break/Continue/Else Constructs¶
So far, we have ignored part of the syntax of the for loop and while loop:
while_stmt ::= "while" assignment_expression ":" suite
["else" ":" suite]for_stmt ::= "for" target_list "in" starred_list ":" suite
["else" ":" suite]namely, the else clause ["else" ":" suite] which also appears in the if statement. How does it work?
Breaking out of a loop¶
Is the following an infinite loop?
while True:
message = input("Input something please:")
if message:
break
print("You entered:", message)Input something please:
Input something please: 1
You entered: 1
The loop is terminated by the break statement when user input is non-empty.
Why is the break statement useful?
Recall the earlier while loop:
while not input("Input something please:"):
passInput something please:
Input something please: 1
This while loop is not useful because it does not store the user input.
Is the break statement strictly necessary?
No, as we can use assignment expressions starting with Python version 3.8.
More generally, we can avoid break statement by using flags, which are boolean variables for flow control:
has_no_input = True
while has_no_input:
message = input("Input something please:")
if message:
has_no_input = False
print("You entered:", message)Input something please:
Input something please: 1
You entered: 1
Using flags sometimes makes the program more readable, and we can use multiple flags for more complicated behavior.
Flags are often named as is_..., has_..., etc.
Continue to Next Iteration¶
What does the following program do?
Is it an infinite loop?
while True:
message = input("Input something please:")
if not message:
continue
print("You entered:", message)The program repeatedly asks the user for input.
If the input is empty, the
continuestatement will skip to the next iteration.The loop can only be terminated by interrupting the kernel.
Such an infinite loop can be useful. E.g., your computer clock continuously updates the current time.
Is the continue statement strictly necessary? Can you rewrite the above
program without the continue statement?
while True:
message = input("Input something please:")
### BEGIN SOLUTION
if message:
print("You entered:", message)
### END SOLUTIONInput something please:
Input something please: 1
You entered: 1
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
Cell In[34], line 2
1 while True:
----> 2 message = input("Input something please:")
3 ### BEGIN SOLUTION
4 if message:
5 print("You entered:", message)
File /opt/conda/lib/python3.14/site-packages/ipykernel/kernelbase.py:1403, in Kernel.raw_input(self, prompt)
1401 msg = "raw_input was called, but this frontend does not support input requests."
1402 raise StdinNotImplementedError(msg)
-> 1403 return self._input_request(
1404 str(prompt),
1405 self._get_shell_context_var(self._shell_parent_ident),
1406 self.get_parent("shell"),
1407 password=False,
1408 )
File /opt/conda/lib/python3.14/site-packages/ipykernel/kernelbase.py:1448, in Kernel._input_request(self, prompt, ident, parent, password)
1445 except KeyboardInterrupt:
1446 # re-raise KeyboardInterrupt, to truncate traceback
1447 msg = "Interrupted by user"
-> 1448 raise KeyboardInterrupt(msg) from None
1449 except Exception:
1450 self.log.warning("Invalid Message:", exc_info=True)
KeyboardInterrupt: Interrupted by userElse construct for a loop¶
The following program checks whether a number is composite, namely,
a positive integer that is
a product of two strictly smaller positive integers.
@interact(num="1")
def check_composite(num):
if num.isdigit():
num = int(num)
for divisor in range(2, num): # why starts from 2 instead of 1
if not num % divisor:
print("It is composite.")
break # where will this go?
else:
print("It is not composite.") # how to get here?
else:
print("Not a positive integer.") # how to get here?There are two else clauses in the earlier code. Which one is for the loop and when is it called?
Solution to Exercise 10
The first else clause that
print('It is not composite.').The clause is called when there is no divisor found in the range from
2tonum.
Try stepping through execution to understand the flow of the program:
%%pytutor
def check_composite(num):
if num.isdigit():
num = int(num)
for divisor in range(2, num):
if not num % divisor:
print("It is composite.")
break
else:
print("It is not composite.")
else:
print("Not a positive integer.")
check_composite("1")
check_composite("2")
check_composite("3")
check_composite("4")The for loop has an else clause that is executed only when the loop terminates normally, i.e., not by a break.
Convert the for loop to a while loop. Try to make the code as efficient as possible with less computation and storage.
@interact(num="1")
def check_composite(num):
if num.isdigit():
num = int(num)
### BEGIN SOLUTION
# for divisor in range(2,num):
divisor = math.isqrt(num) # int(num**0.5) may fail for large integers due to float precision
while divisor > 1:
if num % divisor:
divisor -= 1 # why not go from 2 to int(num ** 0.5)?
else:
print("It is composite.")
break
else:
print("It is not composite.")
### END SOLUTION
else:
print("Not a positive integer.")Another way you will learn later in the course is to write functions and recursions.
If
iis defined before the for loop, its value will be overwritten.As a challenge, can you rewrite the infinite loop
while True: passas aforloop? You will learn later in the course that this is possible if you so desire!The
iterfunction is used to create an iterator from an iterable, such as a range object. Each iteration of thewhileloop calls thenextfunction on the iterator to retrieve the next value from the iterable.If there are no more next values, the iterator raises a
StopIterationexception. Theexceptblock is used to catch this exception to prevent it from bubbling up further and crashing the program. Thetryblock specifies the code to monitor for the exception to catch.