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.

In [1]:
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).

In [2]:
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.

In [ ]:
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.

In [3]:
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 at 0 and going up to n-1.
  • range(m,n) will give a sequence of integers starting at m and going up to n-1.
  • range(m,n,k) will give a sequence of integers starting m, stepping by k, and stopping before n.
In [4]:
for i in range(10):
    print(i)
0
1
2
3
4
5
6
7
8
9
In [5]:
for i in range(3,10):
    print(i)
3
4
5
6
7
8
9
In [6]:
for i in range(3,10,2):
    print(i)
3
5
7
9

Note: the range does generate a list.

In [7]:
[0,1,2,3]
Out[7]:
[0, 1, 2, 3]
In [10]:
range(4)
Out[10]:
range(0, 4)

Exercise: Write Python code to print the cubes of the first 50 positive integers.

In [11]:
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>.

In [12]:
my_list = [1,2,3]
print(my_list)

my_list.append(4)
print(my_list)
[1, 2, 3]
[1, 2, 3, 4]
In [14]:
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.

In [15]:
cubes = []

for i in range(1,51):
    cubes.append(i**3)
In [16]:
cubes
Out[16]:
[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.

In [17]:
3 < 5
Out[17]:
True
In [18]:
3 < 1
Out[18]:
False
In [19]:
5 > 2
Out[19]:
True

We can use <= or >= for less than/greater than or equal to:

In [27]:
2 <= 2
Out[27]:
True
In [28]:
2 < 2
Out[28]:
False
In [29]:
3 >= 2
Out[29]:
True

Inequality checks can be chained together:

In [30]:
2 < 4 <= 6
Out[30]:
True
In [31]:
2 < 4 > 3
Out[31]:
True

We can use a double equality == to check whether two objects are equal to one another:

In [32]:
n = 5

n == 5
Out[32]:
True
In [33]:
3 == 4
Out[33]:
False

We can also check whether two lists are equal to one another:

In [34]:
[1,2,3] == [1,2,3]
Out[34]:
True
In [35]:
[1,2,3] == [1,2,4]
Out[35]:
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.

In [41]:
n = 5

if n % 2 == 0:
    print('{} is even.'.format(n))
print('Done')
Done
In [42]:
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:

In [43]:
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
In [44]:
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
In [46]:
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
In [48]:
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.

In [49]:
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
In [51]:
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.

In [54]:
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.
In [56]:
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.
In [57]:
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.
In [58]:
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.

In [62]:
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.

In [72]:
N = 100

cubes = []
for i in range(1,N+1):
    cube = i**3
    cubes.append(cube)
In [73]:
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.

In [77]:
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.
In [79]:
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.

In [81]:
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.

In [88]:
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.
In [87]:
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.