Wednesday, September 3rd, 2025¶
Working with loops in Python¶
Last week, we wrote nested loops that iterated through all combinations of integers from the two lists [1,2,3]
and [4,5,6]
and printed out the sum for each combination.
for n in [1,2,3]:
for m in [4,5,6]:
print('{} + {} = {}'.format(n,m,n+m))
print('Done with n={}'.format(n))
print('Done')
1 + 4 = 5 1 + 5 = 6 1 + 6 = 7 Done with n=1 2 + 4 = 6 2 + 5 = 7 2 + 6 = 8 Done with n=2 3 + 4 = 7 3 + 5 = 8 3 + 6 = 9 Done with n=3 Done
What if we wanted to iterate through pairs from each list? That is, suppose we want to consider the lists in parallel and iterate through the three pairs (1,4)
, (2,5)
, and (3,6)
.
n_list = [1,2,3]
m_list = [4,5,6]
for i in [0,1,2]:
n = n_list[i]
m = m_list[i]
print('{} + {} = {}'.format(n,m,n+m))
1 + 4 = 5 2 + 5 = 7 3 + 6 = 9
Later on, we'll see how to use the zip
function to achieve this goal in a more natual (and extendable) way.
The range
function¶
We can use other types of iterables to setup for
loops. In the examples above, we've been iterating through a pre-defined list. Suppose we want to perform some operation on the first 10,000 positive integers.
for n in [1,2,3,4,5,6,7,...]
Of course, it's not reasonable for us to write down a list of the first 10,000 positive integers in order to iterate through them. Instead, we can use the range
function.
Note: We can use the help
function to learn more about something in Python. For example, help(range)
will tell us about the range
function.
help(range)
Help on class range in module builtins: class range(object) | 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). | | Methods defined here: | | __bool__(self, /) | True if self else False | | __contains__(self, key, /) | Return bool(key in self). | | __eq__(self, value, /) | Return self==value. | | __ge__(self, value, /) | Return self>=value. | | __getattribute__(self, name, /) | Return getattr(self, name). | | __getitem__(self, key, /) | Return self[key]. | | __gt__(self, value, /) | Return self>value. | | __hash__(self, /) | Return hash(self). | | __iter__(self, /) | Implement iter(self). | | __le__(self, value, /) | Return self<=value. | | __len__(self, /) | Return len(self). | | __lt__(self, value, /) | Return self<value. | | __ne__(self, value, /) | Return self!=value. | | __reduce__(self, /) | Helper for pickle. | | __repr__(self, /) | Return repr(self). | | __reversed__(self, /) | Return a reverse iterator. | | count(self, object, /) | rangeobject.count(value) -> integer -- return number of occurrences of value | | index(self, object, /) | rangeobject.index(value) -> integer -- return index of value. | Raise ValueError if the value is not present. | | ---------------------------------------------------------------------- | Static methods defined here: | | __new__(*args, **kwargs) | Create and return a new object. See help(type) for accurate signature. | | ---------------------------------------------------------------------- | Data descriptors defined here: | | start | | step | | stop
In particular:
range(n)
will give a sequence of integers starting at0
and going up ton-1
.range(m,n)
will give a sequence of integers starting atm
and going up ton-1
.range(m,n,k)
will give a sequence of integers startingm
, stepping byk
, and stopping beforen
.
for i in range(10):
print(i)
0 1 2 3 4 5 6 7 8 9
for i in range(3,10):
print(i)
3 4 5 6 7 8 9
for i in range(3,10,2):
print(i)
3 5 7 9
Note: the range
does generate a list.
[0,1,2,3]
[0, 1, 2, 3]
range(4)
range(0, 4)
Exercise: Write Python code to print the cubes of the first 50 positive integers.
for i in range(1,51):
print(i**3)
1 8 27 64 125 216 343 512 729 1000 1331 1728 2197 2744 3375 4096 4913 5832 6859 8000 9261 10648 12167 13824 15625 17576 19683 21952 24389 27000 29791 32768 35937 39304 42875 46656 50653 54872 59319 64000 68921 74088 79507 85184 91125 97336 103823 110592 117649 125000
So far, we've explicitly generated lists using square brackets and comma-separated inputs. Suppose we want to generate a list containing the cubes of the first 50 positive integers. Our current strategy is not reasonable for this sort of task.
The .append
method (attached to a list) can be used to add an element to a list. That is, we can write something like <some list>.append(<some new element>)
to add <some new element>
to <some list>
.
my_list = [1,2,3]
print(my_list)
my_list.append(4)
print(my_list)
[1, 2, 3] [1, 2, 3, 4]
my_list = []
for i in range(2,20,3):
my_list.append(i)
print(my_list)
[2, 5, 8, 11, 14, 17]
To build a list of the cubes of the first 50 positive integers, we can start with an empty list []
and then iteratively use the .append
method to add elements to that list.
cubes = []
for i in range(1,51):
cubes.append(i**3)
cubes
[1, 8, 27, 64, 125, 216, 343, 512, 729, 1000, 1331, 1728, 2197, 2744, 3375, 4096, 4913, 5832, 6859, 8000, 9261, 10648, 12167, 13824, 15625, 17576, 19683, 21952, 24389, 27000, 29791, 32768, 35937, 39304, 42875, 46656, 50653, 54872, 59319, 64000, 68921, 74088, 79507, 85184, 91125, 97336, 103823, 110592, 117649, 125000]
Suppose we want to count how many of the first 50 cubes end in a digit of 1
. That means we want iterate through each of our cubes, and then somehow decide whether or not it ends in a 1
. This leads us to Boolean expressions and if
statements.
Boolean expressions¶
There are two Boolean values, namely True
and False
.
We can write statements that evalute to either True
or False
called Boolean expressions. For example, we can compare two numbers using <
or >
to see if one is less than the other or one is greater than the other.
3 < 5
True
3 < 1
False
5 > 2
True
We can use <=
or >=
for less than/greater than or equal to:
2 <= 2
True
2 < 2
False
3 >= 2
True
Inequality checks can be chained together:
2 < 4 <= 6
True
2 < 4 > 3
True
We can use a double equality ==
to check whether two objects are equal to one another:
n = 5
n == 5
True
3 == 4
False
We can also check whether two lists are equal to one another:
[1,2,3] == [1,2,3]
True
[1,2,3] == [1,2,4]
False
We can use an if
statement to perform some operations only when a Boolean expression is True
. Again, spacing is CRITICAL, as it indicates which operations are part of the if
statement (which will only run when the Boolean expression is True
), and which operations will run regardless.
n = 5
if n % 2 == 0:
print('{} is even.'.format(n))
print('Done')
Done
n = 6
if n % 2 == 0:
print('{} is even.'.format(n))
print('Done')
6 is even. Done
We very often want to perform different operations based on several Boolean expressions. We can supplement an if
statement with an elif
statement (which is short for "else if") with a new Boolean expression to perform operations only in the case that the first if
expression was False
and the new expression is True
:
n = 5
if n % 2 == 0:
print('{} is even.'.format(n))
elif n % 2 == 1:
print('{} is odd.'.format(n))
print('Done')
5 is odd. Done
n = 6
if n % 2 == 0:
print('{} is even.'.format(n))
elif n % 2 == 1:
print('{} is odd.'.format(n))
print('Done')
6 is even. Done
n = 6
if n % 2 == 0:
print('{} is even.'.format(n))
elif 1 == 1:
print('{} is odd.'.format(n))
print('Done')
6 is even. Done
n = 13
if n % 3 == 0:
print('{} has remainder 0 after division by 3.'.format(n))
elif n % 3 == 1:
print('{} has remainder 1 after division by 3.'.format(n))
elif n % 3 == 2:
print('{} has remainder 2 after division by 3.'.format(n))
print('Done')
13 has remainder 1 after division by 3. Done
We can also use an else
statement to perform some operations if none of the if
or elif
Boolean expressions were True
.
n = 13
if n % 3 == 0:
print('{} has remainder 0 after division by 3.'.format(n))
elif n % 3 == 1:
print('{} has remainder 1 after division by 3.'.format(n))
else:
print('{} has remainder 2 after division by 3.'.format(n))
print('Done')
13 has remainder 1 after division by 3. Done
n = 5
if n%2 == 0:
print('{} is even.'.format(n))
else:
print('{} is odd.'.format(n))
5 is odd.
Exercise: Count how many cubes of the first 50 positive integers end in a digit of 1.
count = 0
for i in range(1,51):
if i**3 % 10 == 1:
count = count + 1
print('{} of the first 50 cubes end with 1.'.format(count))
5 of the first 50 cubes end with 1.
cubes_that_end_in_1 = []
for i in range(1,51):
if i**3 % 10 == 1:
cubes_that_end_in_1.append(i**3)
print('{} of the first 50 cubes end with 1.'.format(len(cubes_that_end_in_1)))
5 of the first 50 cubes end with 1.
Some thoughts about efficiency:
- In the second version, we are computing
i**3
twice. It would be faster to compute it once, save the result, then re-use that saved result as necessary. - We've previously calculated and stored a list of the first 50 cubes. It would more efficient to make use of that list instead of re-computing the same cubes.
cubes_that_end_in_1 = []
for i in range(1,51):
cube = i**3
if cube % 10 == 1:
cubes_that_end_in_1.append(cube)
print('{} of the first 50 cubes end with 1.'.format(len(cubes_that_end_in_1)))
5 of the first 50 cubes end with 1.
count = 0
for cube in cubes:
if cube % 10 == 1:
count = count + 1
print('{} of the first 50 cubes end with 1.'.format(count))
5 of the first 50 cubes end with 1.
One other modification that would be nice: the print statement has the number 50
handwritten in, and so the for
loop.
N = 100
cubes_that_end_in_1 = []
for i in range(1,N+1):
cube = i**3
if cube % 10 == 1:
cubes_that_end_in_1.append(cube)
print('{} of the first {} cubes end with 1.'.format(len(cubes_that_end_in_1),N))
10 of the first 100 cubes end with 1.
Exercise: Modify the previous cell to count the number of cubes whose remainder after dividing by 4
is 0
.
N = 100
cubes = []
for i in range(1,N+1):
cube = i**3
cubes.append(cube)
rem = 1
den = 4
cubes_that_end_in_rem = []
for cube in cubes:
if cube % den == rem:
cubes_that_end_in_rem.append(cube)
print('{} of the first {} cubes have remainder {} after division by {}.'.format(len(cubes_that_end_in_rem),N, rem, den))
25 of the first 100 cubes have remainder 1 after division by 4.
Exercise: Modify your code from the previous exercise to also count the number of cubes whose remainder after dividing by 4
is 1
, 2
, and 3
respectively.
den = 7
N = 1000
cubes = []
for i in range(1,N+1):
cube = i**3
cubes.append(cube)
for rem in range(den):
cubes_that_end_in_rem = []
for cube in cubes:
if cube % den == rem:
cubes_that_end_in_rem.append(cube)
print('{} of the first {} cubes have remainder {} after division by {}.'.format(len(cubes_that_end_in_rem),N, rem, den))
142 of the first 1000 cubes have remainder 0 after division by 7. 429 of the first 1000 cubes have remainder 1 after division by 7. 0 of the first 1000 cubes have remainder 2 after division by 7. 0 of the first 1000 cubes have remainder 3 after division by 7. 0 of the first 1000 cubes have remainder 4 after division by 7. 0 of the first 1000 cubes have remainder 5 after division by 7. 429 of the first 1000 cubes have remainder 6 after division by 7.
N = 1000
cubes = []
for i in range(1,N+1):
cube = i**3
cubes.append(cube)
for den in range(2,11):
for rem in range(den):
cubes_that_end_in_rem = []
for cube in cubes:
if cube % den == rem:
cubes_that_end_in_rem.append(cube)
print('{} of the first {} cubes have remainder {} after division by {}.'.format(len(cubes_that_end_in_rem),N, rem, den))
print()
500 of the first 1000 cubes have remainder 0 after division by 2. 500 of the first 1000 cubes have remainder 1 after division by 2. 333 of the first 1000 cubes have remainder 0 after division by 3. 334 of the first 1000 cubes have remainder 1 after division by 3. 333 of the first 1000 cubes have remainder 2 after division by 3. 500 of the first 1000 cubes have remainder 0 after division by 4. 250 of the first 1000 cubes have remainder 1 after division by 4. 0 of the first 1000 cubes have remainder 2 after division by 4. 250 of the first 1000 cubes have remainder 3 after division by 4. 200 of the first 1000 cubes have remainder 0 after division by 5. 200 of the first 1000 cubes have remainder 1 after division by 5. 200 of the first 1000 cubes have remainder 2 after division by 5. 200 of the first 1000 cubes have remainder 3 after division by 5. 200 of the first 1000 cubes have remainder 4 after division by 5. 166 of the first 1000 cubes have remainder 0 after division by 6. 167 of the first 1000 cubes have remainder 1 after division by 6. 167 of the first 1000 cubes have remainder 2 after division by 6. 167 of the first 1000 cubes have remainder 3 after division by 6. 167 of the first 1000 cubes have remainder 4 after division by 6. 166 of the first 1000 cubes have remainder 5 after division by 6. 142 of the first 1000 cubes have remainder 0 after division by 7. 429 of the first 1000 cubes have remainder 1 after division by 7. 0 of the first 1000 cubes have remainder 2 after division by 7. 0 of the first 1000 cubes have remainder 3 after division by 7. 0 of the first 1000 cubes have remainder 4 after division by 7. 0 of the first 1000 cubes have remainder 5 after division by 7. 429 of the first 1000 cubes have remainder 6 after division by 7. 500 of the first 1000 cubes have remainder 0 after division by 8. 125 of the first 1000 cubes have remainder 1 after division by 8. 0 of the first 1000 cubes have remainder 2 after division by 8. 125 of the first 1000 cubes have remainder 3 after division by 8. 0 of the first 1000 cubes have remainder 4 after division by 8. 125 of the first 1000 cubes have remainder 5 after division by 8. 0 of the first 1000 cubes have remainder 6 after division by 8. 125 of the first 1000 cubes have remainder 7 after division by 8. 333 of the first 1000 cubes have remainder 0 after division by 9. 334 of the first 1000 cubes have remainder 1 after division by 9. 0 of the first 1000 cubes have remainder 2 after division by 9. 0 of the first 1000 cubes have remainder 3 after division by 9. 0 of the first 1000 cubes have remainder 4 after division by 9. 0 of the first 1000 cubes have remainder 5 after division by 9. 0 of the first 1000 cubes have remainder 6 after division by 9. 0 of the first 1000 cubes have remainder 7 after division by 9. 333 of the first 1000 cubes have remainder 8 after division by 9. 100 of the first 1000 cubes have remainder 0 after division by 10. 100 of the first 1000 cubes have remainder 1 after division by 10. 100 of the first 1000 cubes have remainder 2 after division by 10. 100 of the first 1000 cubes have remainder 3 after division by 10. 100 of the first 1000 cubes have remainder 4 after division by 10. 100 of the first 1000 cubes have remainder 5 after division by 10. 100 of the first 1000 cubes have remainder 6 after division by 10. 100 of the first 1000 cubes have remainder 7 after division by 10. 100 of the first 1000 cubes have remainder 8 after division by 10. 100 of the first 1000 cubes have remainder 9 after division by 10.
Prime numbers¶
Our first project will deal with prime numbers. It will be useful if we can develop some code to decide whether a given integer is prime or not. One strategy for checking whether a given number is prime or not is to look for numbers that evenly divide it. That is, we can test a number $n$ for primality by looking for numbers $d$ such that the remainder from dividing $n$ by $d$ is zero.
n = 9
d = 3
if n % d == 0:
print('{} is not prime, it has divisor {}'.format(n,d))
9 is not prime, it has divisor 3
Exercise: Write code that will set a Boolean variable to True
if an integer n
is prime and will set the Boolean variable to False
if not.
n = 8
is_prime = True
for d in range(2,n):
if n % d == 0:
is_prime = False
if not is_prime:
print('{} is not prime.'.format(n))
else:
print('{} is prime.'.format(n))
8 is not prime.
n = 8
divisors = []
for d in range(2,n):
if n % d == 0:
divisors.append(d)
is_prime = (len(divisors) == 0)
if not is_prime:
print('{} is not prime, it has divisors {}.'.format(n,divisors))
else:
print('{} is prime.'.format(n))
8 is not prime, it has divisors [2, 4].
Note: in the code above, it will often happen that we perform many divisibility checks for no reason (for example, if 2
divides n
, then we will still unnecessarily check whether 3
, 4
, 5
, ... also divide n
). It would be helpful if we could escape the loop prematurely.