6. Loops

Loops are used to recycle logic over and over over any number of data or inputs. Without loops, for as many times as you need to apply the same logic to your data, you will have to code (or copy and paste) your logic that many times. Loops are frequently used to iterate over collections such as lists, sets, tuples and maps. There are two basic loops, the while and for-each loops. Let’s investigate them further below.

6.1. while

The while loop has the following syntax.

1while <some-condition-is-true>:
2   # perform some logic

Below, we loop until the variable n is equal to or greater than 10. At the end of the block of code under the while loop, we increment n by one. If we did not increment n then the termination condition will never be False and we would loop forever.

1n = 0
2
3while n < 10:
4    print(n)
5    n += 1

You can also loop endless and break out of a while loop manually. In the example below, we loop forever since we set while True:. However, inside the loop, we check if n is a multiple of 10, and if so, we issue a break (a manual termination) of the while loop.

1n = 0
2while True:
3   print(n)
4   n += 1
5   if n % 10 == 0:
6      break

6.1.1. Exercise

Create a program to keep asking for the user to input something (anything) 5 times. Use a while loop to help you in this exercise. Inside the loop, simply print back out what the user entered.

Solution.

1n = 0
2while n < 5:
3   user_input = input('enter in anything: ')
4   print(f'{user_input}')

6.1.2. Exercise

Create a program using a while loop to keep asking for the user to input something endlessly. Inside the loop, if the user enters in n, then break. If the user did not enter n as the input, then echo the user’s input back to the terminal.

Solution.

1while True:
2   user_input = input('enter in anything: ')
3
4   if user_input == 'n':
5      break
6
7   print(f'{user_input}')

6.2. for-each

The for-each loop is best understood when looping over the elements of a collections. To appreciate the power of the for-each loop, let’s see what it would take to print a list of 10 numbers without a for-each loop.

 1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 2
 3print(numbers[0])
 4print(numbers[1])
 5print(numbers[2])
 6print(numbers[3])
 7print(numbers[4])
 8print(numbers[5])
 9print(numbers[6])
10print(numbers[7])
11print(numbers[8])
12print(numbers[9])

This approach to printing each number from a list is not scalable if there are a million numbers to print. We need to use the for-each loop. The syntax of the for-each loop is as follows.

1for <element> in <collection>:
2   # perform some logic over the element

6.2.1. Basic for-each

Here are some examples of using a for-each loop against different types of collections.

 1# looping over an array/list
 2number_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 3
 4for number in number_list:
 5    print(number)
 6
 7# looping over a set
 8number_set = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
 9
10for number in number_set:
11    print(number)
12
13# looping over a tuple
14number_tuple = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
15
16for number in number_tuple:
17    print(number)
18
19# looping over a map
20number_map = {
21    'a': 0,
22    'b': 1,
23    'c': 2
24}
25
26for key, val in number_map.items():
27    print(key, val)

6.2.2. Exercise

Loop through each number in the list below and print the number and what the number times 2 would be.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Solution.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4   n = number * 2
5   print(f'{number} x 2 = {n}')

6.2.3. Exercise

Loop through each number in the list below and print the number and what the number times 2 would be only if the number is even.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Solution.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4   if number % 2 == 0:
5      n = number * 2
6      print(f'{number} x 2 = {n}')

6.2.4. Exercise

Loop through each number in the list below and print the number and what the number times 3 would be only if the number times 3 is odd.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Solution.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4   n = number * 3
5   if n % 2 != 0:
6      print(f'{number} x 3 = {n}')

6.2.5. Exercise

Loop through each key-value pair in the following dictionary. Since the values are tuples, loop through each element in the tuple and print them.

1data = {
2   'fred': (28, 150.5, 5.5),
3   'john': (32, 180.2, 6.2)
4}

Solution.

1data = {
2   'fred': (28, 150.5, 5.5),
3   'john': (32, 180.2, 6.2)
4}
5
6for key, tup in data.items():
7   for item in tup:
8      print(item)

6.2.6. for-each with break

Breaking is also possible inside a for-each loop.

1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4    if number == 3:
5        print('found 3')
6        break

6.2.7. Exercise

Loop through the following list and break after the third odd number is encountered.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Solution.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3total_odds = 0
4
5for number in numbers:
6   if number % 2 != 0:
7      total_odds += 1
8   if total_odds == 3:
9      break

6.2.8. for-each with continue

When used inside a for-each or while loop, the continue command forces the logic back to the start of the code block inside the loop; all code below the continue are skipped. The code below iterates through each integer a list and skips printing the integer if it is odd.

1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for number in numbers:
4    if number % 2 != 0:
5        continue
6    print(number)
7

6.2.9. Exercise

Loop through the following list of names and skip printing names that do not start with a j. Use the continue command to achieve this request.

1names = ['jane', 'mary', 'josephine', 'nancy']

Solution.

1names = ['jane', 'mary', 'josephine', 'nancy']
2
3for name in names:
4   if not name.startswith('j'):
5      continue
6   print(name)

6.2.10. for-each with enumeration

Sometimes, we want to access the corresponding index of an element as we are iterating through a collection. To get the index and element, we can pass the collection to the enumerate() function.

1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for i, number in enumerate(numbers):
4    print(i, number)

Note that enumerate() will return a tuple, and so we can loop over the elements as follows.

1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for tup in enumerate(numbers):
4   print(tup[0], tup[1])

Or, we can unpack the tuple inside the for-each loop.

1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2
3for tup in enumerate(numbers):
4   i, number = tup
5   print(i, number)

Typically, we prefer to unpack the tuple with the for-each loop as for i, numbers in enumerate(numbers):.

If we did not want to start the index at 0, then we can specify a starting index as follows.

 1numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 2
 3for i, number in enumerate(numbers, 10):
 4   print(i, number)
 5
 6# the code above will print
 7# 10 0
 8# 11 1
 9# 12 2
10# and so on ...

6.2.11. Exercise

Loop through the following list of names and print the name only if the name starts with a j and the associated index is odd.

1names = ['jane', 'mary', 'josephine', 'jack', 'nancy']

Solution.

1names = ['jane', 'mary', 'josephine', 'jack', 'nancy']
2
3for i, name in enumerate(names):
4   if name.startswith('j') and i % 2 != 0:
5      print(name)

6.2.12. Looping over two lists

What if we want to iterate over 2 collections at the same time? We can use the zip() function. zip() aligns the elements from 2 or more lists and creates a tuple for each aligned elements. We can create a list from zipping two lists as follows.

1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3
4persons = list(zip(names, ages))
5print(persons)
6
7# should print [('Jack', 18), ('John', 19), ('Joe', 20)]

The example below show the use of using a for-each loop with zip(). Note how we unpack the tuple elements generated by zip().

1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3
4for name, age in zip(names, ages):
5    print(name, age)

6.2.13. Exercise

Loop over the two lists below simultaneously using zip() and print the name and age only if the age is even.

1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]

Solution.

1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3
4for name, age in zip(names, ages):
5   if age % 2 == 0:
6      print(f'{name}, {age}')

6.2.14. Looping with enumerate and zip

We can combine enumerate() with zip() as follows.

1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3persons = list(enumerate(zip(names, ages)))
4print(persons)
5
6# the above should print [(0, ('Jack', 18)), (1, ('John', 19)), (2, ('Joe', 20))]
7# note the result is a list of tuples, where each tuple has the index in the first
8# position and a tuple in the second position

Here is how we can use enumerate() with zip() with a for-each loop.

1names = ['Jack', 'John', 'Joe']
2ages = [18, 19, 20]
3
4for i, (name, age) in enumerate(zip(names, ages)):
5    print(i, name, age)

6.2.15. Exercise

Loop through both lists below. Only print the name and age if the name starts with a J, the index is odd and the age is odd.

1names = ['Mary', 'Jack', 'John', 'Nancy', 'Sam', 'Jeremy', 'Mark']
2ages = [18, 19, 21, 24, 26, 27, 32]
1names = ['Mary', 'Jack', 'John', 'Nancy', 'Sam', 'Jeremy', 'Mark']
2ages = [18, 19, 21, 24, 26, 27, 32]
3
4for i, (name, age) in enumerate(zip(names, ages)):
5   if i % 2 != 0 and age % 2 != 0 and name.startswith('J'):
6      print(f'{i}, {name}, {age}')

6.3. Comprehension

Comprehensions are a way to transform an existing collection into a new one using the for-each loop. There are comprehensions to generate lists, sets and dictionaries.

6.3.1. List comprehension

The list comprehension has the following syntax: [<expression> for <element> in <collection> <filter:optional>]. The <expression> is required and the <filter:optional> is optional. The <expression> is typically a transformation of the <element>. Let’s say we have a list numbers = [1, 2, 3, 4] and we want to transform this list into a new one where each element is multiplied by two. We can use a regular for-each loop or a list comprehension.

1# do not create a list this way
2
3numbers = []
4for num in [1, 2, 3, 4]:
5    n = num * 2
6    numbers.append(n)
7
8# use a list comprehension
9numbers = [num * 2 for num in [1, 2, 3, 4]]

An example of using a filter is as follows. In this example, we transform each integer to a new number by multiplying it by 3 and we filter for for only odd-indexed numbers.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2nums = [number * 3 for i, number in numbers if i % 2 != 0]

6.3.2. Exercise

Transform the list below into a new one with all boolean values, where each element in the new list indicates if the corresponding element in the existing list is even True or odd False.

1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2nums = [True if number % 2 == 0 else False for number in numbers]

6.3.3. Nested list comprehension

List comprehensions can also be nested as in the example below. Note that we have a list of lists, which represents a two-dimensional matrix. So we have to iterate over the rows and then the columns in that row. The result is that we end up with a flattened list (the new list is not like the first list, which is a list of lists).

1matrix = [[1, 2], [3, 4], [5, 6]]
2
3flattened = [col for row in matrix for col in row]
4
5# flattened = [1, 2, 3, 4, 5, 6]

6.3.4. Exercise

Flatten the matrix below using a nested list comprehension, but, only include those elements whose row and column indices are equal.

1matrix = [[1, 2, 3], [3, 4, 5], [5, 6, 7]]

Solution.

1matrix = [[1, 2, 3], [3, 4, 5], [5, 6, 7]]
2diagonal = [col for i, row in enumerate(matrix) for j, col in enumerate(row) if i == j]

6.3.5. Set comprehension

Set comprehensions are just like list comprensions, except, instead of using [] to setup the comprehension, we use {}. The set comprehension has the following syntax: {<expression> for <element> in <collection> <filter:optional>}.

1# do not create a set in this way
2
3s = set()
4for name in ['Jack', 'John', 'Joe', 'Mary']:
5    n = len(name)
6    s.add(n)
7
8# create a set in this way, using a set comprehension
9s = {len(name) for name in ['Jack', 'John', 'Joe', 'Mary']}

6.3.6. Exercise

Find all the unique odd numbers in the following list of numbers.

1numbers = [1, 2, 3, 4, 5, 2, 3, 5, 1, 1]

Solution.

1numbers = [1, 2, 3, 4, 5, 2, 3, 5, 1, 1]
2nums = {number for number in numbers if number % 2 != 0}

6.3.7. Dictionary comprehension

A dictionary comprehension is used to generate a new dictionary. The dictionary comprehension has the following syntax: {<key>: <expression> for <element> in <collection> <filter:optional>}.

 1# do not create a map in this way
 2names = ['Jack', 'John', 'Joe', 'Mary']
 3
 4m = {}
 5for name in names:
 6    m[name] = len(name)
 7
 8
 9# create a map in this way, using a map comprehension
10names = ['Jack', 'John', 'Joe', 'Mary']
11
12m = {name: len(name) for name in names}

6.3.8. Exercise

Below is a list of names. Note that the elements are tuples, where the first element in the tuple is the first name and the second element is the last name. Transform the list into a dictionary where they key is the initials of each name and the corresponding value is the full name.

1names = [('Jack', 'Washington'), ('John', 'Doe'), ('Joe', 'Black'), ('Mary', 'Swanson')]

Solution.

1names = [('Jack', 'Washington'), ('John', 'Doe'), ('Joe', 'Black'), ('Mary', 'Swanson')]
2persons = {f'{fname[0]}.{lname[0]}.': f'{fname} {lname}' for fname, lname in names}
3print(persons)
4
5# should print {'J.W.': 'Jack Washington', 'J.D.': 'John Doe', 'J.B.': 'Joe Black', 'M.S.': 'Mary Swanson'}

6.3.9. Generator comprehension

With list, set and dictionary comprehensions, the resulting data is evaluated and created immediately. The generator comprehension, however, only evaluates the expression as needed. The former comprehensions are eagerly evaluated and the latter comprehension is lazily evaluated. If we need to generate a new collection that does not need to be stored, use a generator comprehension as intermediary results will not be stored. The syntax for a generator comprehension is nearly identical to the list comprehension, except that we use () instead of []: (<expression> for <element> in <collection> <filter:optional>)

1# do not do this, it's not memory efficient
2x = (10, ) * 100000
3y = [num * 2 for num in x]
4
5# do this instead, use a generator comprehension
6x = (10, ) * 100000
7y = (num * 2 for num in x)

6.3.10. Exercise

Write a program to ask the user to input a number over and over (until the user requests to quit by entering q to quit). Each time a user enters a number, whatever that number is, compute the list of numbers that results when multiplying it by 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.

Solution.

 1# we use a tuple for the collection
 2# why? because we do not want this collection to ever be modified
 3# we define this collection of numbers outside the while loop
 4# why? because we only need to define it once, not over and over inside the loop
 5multipliers = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
 6
 7while True:
 8   user_input = input('enter a number or "q" to quit: ')
 9
10   if user_input == 'q':
11      break
12
13   num = int(user_input)
14   numbers = [n * num for n in multipliers]
15   print(numbers)