Monday, August 31st, 2026¶

Last week, we started working with different Python datatypes, including:

  • Integers (int)
  • Floats (float)
  • Strings (str)
  • Lists (list)

We had just started looking at how we can iterate through a list to perform some actions on each of its elements.

Working with loops in Python¶

It often happens that we want to perform the same (or similar) operations many times. We can perform iterative operations using a for loop. For example, we can iterate through the items in a list and perform some desired operations.

The syntax for writing a for loop is: for (some variable name) in (some iterable object): (do something)

The variable (some variable name) will sequentially take on each of the values stored in (some iterable object) (a list, for example), and for each value will (do something).

In [48]:
for num in [1,2,3,4,5]:
    print(num)
1
2
3
4
5

Key info: The spacing in Python is critical!!!. In particular, the spacing decides what operations are part of a for loop and what operations are not.

In [50]:
for num in [1,2,3,4,5]:
    print(num**2)
print('Done')
1
4
9
16
25
Done

In the cell above, notice how the print('Done') action only occurs after the loop has completed, because it is not indented to be part of the for loop.

In [51]:
for num in [1,2,3,4,5]:
    print(num**2)
    print('Done')
1
Done
4
Done
9
Done
16
Done
25
Done

This time in the cell above, the print('Done') action occurs during every iteration of the loop, because it is indented to be part of the for loop.

We can also use for loops inside other for loops. We call these "nested loops".

Exercise: Write nested for loops that iterate through all combinations of integers from the two lists [1,2,3] and [4,5,6] and print out the sum for each combination.

In [3]:
for n in [1,2,3]:
    for m in [4,5,6]:
        print('{} + {} = {}'.format(n,m,n+m))
1 + 4 = 5
1 + 5 = 6
1 + 6 = 7
2 + 4 = 6
2 + 5 = 7
2 + 6 = 8
3 + 4 = 7
3 + 5 = 8
3 + 6 = 9

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).

Exercise: Define two lists: one that contains a list of first names and another containing the corresponding last names (in the same order). Write a for loop that iterates through the two lists in parallel and prints out the sum of each corresponding pair.

In [11]:
my_list1 = [1,2,3,4,5,6]
my_list2 = [4,5,6,7,8,9]

for i in [0,1,2]:
    n = my_list1[i]
    m = my_list2[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 num in [1,2,3,4,5,6,7,8,

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

Let's test out these different uses with for loops.

In [13]:
for num in range(10):
    print(num)
0
1
2
3
4
5
6
7
8
9
In [14]:
for num in range(4,10):
    print(num)
4
5
6
7
8
9
In [16]:
for num in range(2, 10, 3):
    print(num)
2
5
8
In [21]:
my_list1 = [1,2,3,4,5,6]
my_list2 = [4,5,6,7,8,9]

for i in range(len(my_list1)):
    n = my_list1[i]
    m = my_list2[i]

    print('{} + {} = {}'.format(n,m,n+m))
1 + 4 = 5
2 + 5 = 7
3 + 6 = 9
4 + 7 = 11
5 + 8 = 13
6 + 9 = 15
In [18]:
range(10)
Out[18]:
range(0, 10)

Note: the range doesn't exactly generate a list. Instead, it is what's called a generator (i.e. instructions on how to construct a sequence of values).

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

In [22]:
for num in range(1,51):
    print(num**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

Constructing lists¶

So far, we've explicitly generated lists using square brackets and comma-separated inputs (which we've had to manually type in). 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 [23]:
my_list = [1,2,3]
print(my_list)
[1, 2, 3]
In [24]:
my_list.append('four')
In [25]:
print(my_list)
[1, 2, 3, 'four']

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 [26]:
cubes = []

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

Exercise: Generate a list of the squares of the first $40$ positive integers. Then print the remainder of each after division by $7$.

In [28]:
squares = []

for num in range(1,41):
    squares.append(num**2)
In [31]:
for square in squares:
    print(square % 7)
1
4
2
2
4
1
0
1
4
2
2
4
1
0
1
4
2
2
4
1
0
1
4
2
2
4
1
0
1
4
2
2
4
1
0
1
4
2
2
4

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 [32]:
4 < 5
Out[32]:
True
In [33]:
3.2 > 3.1
Out[33]:
True
In [34]:
7 < 4
Out[34]:
False

Similarly, we can use <= or >= for less than/greater than or equal to.

In [35]:
3.2 <= 3.2
Out[35]:
True
In [36]:
7 >= 6.9
Out[36]:
True

Inequality checks can also be chained together.

In [37]:
3 < 5 < 6
Out[37]:
True
In [38]:
3 < 6 < 5
Out[38]:
False

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

In [39]:
5 == 6
Out[39]:
False
In [40]:
5 == 5.0
Out[40]:
True

We can also check whether two lists are equal to one another (i.e. if they contain equal objects in the same order).

In [41]:
my_list1 = [1,2,3]
my_list2 = [1,2,4]
my_list3 = [1,2,3]
In [42]:
my_list1 == my_list2
Out[42]:
False
In [43]:
my_list1 == my_list3
Out[43]:
True

We can construct more complicated Boolean expressions using the and, or, and not operators. That is:

  • (some expression) and (some other expression) will evaluate as True if (some expression) and (some other expression) are both True.
  • (some expression) or (some other expression) will evaluate as True if either (some expression) or (some other expression) are True (or both).
  • not (some expression) will evaluate as True if (some expression) is False.
In [44]:
(3 < 5) and (3.0 < 5.0)
Out[44]:
True
In [45]:
(6 < 7) or (7 < 6)
Out[45]:
True
In [46]:
(6 < 6) or (6 < 6)
Out[46]:
False
In [47]:
not (5 > 1)
Out[47]:
False

In-class Exercise: For each number $n=1,2,3,...,20$, print out True if it is both even and a multiple of $3$, and False otherwise.

Note: A number $n$ is even if it has remainder $0$ after division by $2$. Similarly, $n$ is a multiple of $3$ if it has remainder $0$ after division by $3$.

In [50]:
for num in range(1,21):
    print(num, (num % 2 == 0) and (num % 3 == 0))
1 False
2 False
3 False
4 False
5 False
6 True
7 False
8 False
9 False
10 False
11 False
12 True
13 False
14 False
15 False
16 False
17 False
18 True
19 False
20 False

Using if statements¶

We can use an if statement to perform some operations only when a Boolean expression is True. The syntax for writing an if statement is: if (some Boolean expression): (do something)

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 are outside of the if statement (which will run regardless).

In [53]:
num = 7

if (num % 2 == 0):
    print(num, 'is even')

if (num % 2 == 1):
    print(num, 'is odd')

print('Done')
7 is odd
Done
In [54]:
num = 7

if (num % 3 == 0):
    print(num, 'is a multiple of 3')

if (num % 3 == 1):
    print(num, 'is one more than a multiple of 3')

if (num % 3 == 2):
    print(num, 'is two more than a multiple of 3')

print('Done')
7 is one more than a multiple of 3
Done

Optionally, we can include an else block immediately following an if block. In this case, the code inside the else block will only occur if the Boolean expression in the if statement was False.

In [55]:
num = 7

if (num % 2 == 0):
    print(num, 'is even')
else:
    print(num, 'is odd')

print('Done')
7 is odd
Done

Exercise: For each number $n=1,2,3,...,20$, print out a statement indicating whether the number is even and a multiple of $3$ or not.

In [56]:
for num in range(1,21):
    if (num % 2 == 0) and (num % 3 == 0):
        print(num, 'is both even and a multiple of 3')
    else:
        print(num, 'is not both even and a multiple of 3')
1 is not both even and a multiple of 3
2 is not both even and a multiple of 3
3 is not both even and a multiple of 3
4 is not both even and a multiple of 3
5 is not both even and a multiple of 3
6 is both even and a multiple of 3
7 is not both even and a multiple of 3
8 is not both even and a multiple of 3
9 is not both even and a multiple of 3
10 is not both even and a multiple of 3
11 is not both even and a multiple of 3
12 is both even and a multiple of 3
13 is not both even and a multiple of 3
14 is not both even and a multiple of 3
15 is not both even and a multiple of 3
16 is not both even and a multiple of 3
17 is not both even and a multiple of 3
18 is both even and a multiple of 3
19 is not both even and a multiple of 3
20 is not both even and a multiple of 3

Exercise: Build a list of all numbers less than $100$ that are both even and a multiple of $3$.

In [57]:
even_multiples_of_3 = []

for num in range(100):
    if (num % 2 == 0) and (num % 3 == 0):
        even_multiples_of_3.append(num)
In [58]:
print(even_multiples_of_3)
[0, 6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96]

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 [62]:
num = 101

if num > 10000:
    print(num,'is very large')
elif num > 1000:
    print(num,'is large')
elif num > 100:
    print(num,'is medium')
elif num > 10:
    print(num,'is medium-small')
else:
    print(num,'is small')
101 is medium

Exercise: Use an if/elif/else triple to print a string stating whether an integer n is a multiple of $3$, one more than a multiple of $3$, or two more than a multiple of $3$.

In [63]:
num = 7

if (num % 3 == 0):
    print(num, 'is a multiple of 3')

elif (num % 3 == 1):
    print(num, 'is one more than a multiple of 3')

else:
    print(num, 'is two more than a multiple of 3')

print('Done')
7 is one more than a multiple of 3
Done

Exercise: Out of the squares of the first $40$ positive integers, count how many have remainder $1$ after division by $7$.

In [64]:
squares = []

for num in range(1,41):
    squares.append(num**2)
In [66]:
squares_with_remainder_1 = []

for square in squares:
    if (square % 7 == 1):
        squares_with_remainder_1.append(square)
In [68]:
print(len(squares_with_remainder_1))
11
In [69]:
squares_with_remainder_1
Out[69]:
[1, 36, 64, 169, 225, 400, 484, 729, 841, 1156, 1296]

Some thoughts:

It would be nice if we defined some parameters to control the above cells. That is, it would be nice if we could easily change, say, the divisor that we are using.

In [ ]:
div = 13

squares = []
for num in range(1,41):
    squares.append(num**2)


squares_with_remainder_1 = []
for square in squares:
    if (square % div == 1):
        squares_with_remainder_1.append(square)

num_squares_with_remainder_1 = len(squares_with_remainder_1)

print(num_squares_with_remainder_1,'squares have remainder 1 after division by',div)

Exercise: Count how many of the first $1000$ cubes have remainder $0$ after division by $4$. Use variables (your choice of variable names) to define the parameters $1000$, $0$, and $4$. Your code should print a statement like "W of the first X cubes have remainder Y after division by Z."

In [1]:
N = 1000
div = 4
rem = 0
exp = 3

powers = []
for num in range(1,N+1):
    powers.append(num**exp)


powers_with_rem = []
for power in powers:
    if (power % div == rem):
        powers_with_rem.append(power)

num_powers_with_rem = len(powers_with_rem)

output_str = '{} of the first {} {}th powers have remainder {} after divison by {}.'
print(output_str.format(num_powers_with_rem, N, exp, rem, div))
500 of the first 1000 3th powers have remainder 0 after divison by 4.

Exercise: Use the code above inside a for loop to count how many of the first $1000$ cubes have remainder $0$, then remainder $1$, then remainder $2$, then remainder $3$ after division by $4$.

In [ ]:
 
In [ ]: