Control Flow#

Programs are shaped by control flow, which determines which commands run at which times. Python programs frequently rely on a few key mechanisms of control flow.

For loops#

A common pattern in programming is running some commands multiple times using a loop.

In Python, we can do this using a for loop:

for item in items:
    some_code_to_run_for_each_item

A common pattern is to loop over some items in a list and do something with each item in the list.

words = ["piano", "fountain", "hollow", "pupil"]
for word in words:
    print(word)
piano
fountain
hollow
pupil

Here, word is a new kind of variable we haven’t seen yet. It changes its value at each step of the loop. First, it’s "piano", then "fountain", etc., until the end of the list of words. Each time we run print(word), we’re printing a different word in the list.

Let’s take another look at what happens during a for loop. If we run the following loop:

numbers = [1, 2, 3]
for number in numbers:
    print(number)

Each step of the loop accesses a different part of the list:

numbers = [1, 2, 3]
number  =  ^

number = 1

numbers = [1, 2, 3]
number  =     ^

number = 2

numbers = [1, 2, 3]
number  =        ^

number = 3

We can use for loops for lots of things. One example is to loop over every item in a list and create a new list by modifying that list.

cap = []
for word in words:
    cap.append(word.capitalize())
print(cap)
['Piano', 'Fountain', 'Hollow', 'Pupil']

Here, we first initialized a new list by setting cap = []. This creates an empty list with no items in it. Next, we looped over the list of words. At each step of the loop, we capitalized the word and appended it to the cap list.

Let’s take another look at when building a list using a for loop. If we run the following loop:

letters = ["a", "b"]
cap = []
for letter in letters:
    cap.append(letter.capitalize())

Before the loop, cap is empty:

cap = []

On each step of the loop, we access a letter, capitalize it, and append it to the cap list:

letters = ["a", "b"]
letter  =   ^

cap = ["A"]

letters = ["a", "b"]
letter  =        ^

cap = ["A", "B"]

Exercise: looping over a list#

Write a list called numbers with entries 1, 2, 3, and 4. Use a for loop to iterate over each number and create a new list called squares with each entry squared.

# answer here

Another common approach is to loop over indices. Using the range function, we can loop from 0 to some maximum number.

for i in range(len(words)):
    print(words[i])
piano
fountain
hollow
pupil

Here, instead of looping over the words directly, we create an index i that accesses each part of the words list. The range(len(words)) part is a common way to loop over all indices in a list.

Let’s look at looping based on an index in more detail. If we run the following loop:

numbers = [1, 2, 3]
for i in range(len(numbers)):
    print(i)

Each step of the loop accesses a different index of the list:

numbers = [1, 2, 3]
index   = [0, 1, 2]
i       =  ^

i = 0

numbers = [1, 2, 3]
index   = [0, 1, 2]
i       =     ^

i = 1

numbers = [1, 2, 3]
index   = [0, 1, 2]
i       =        ^

i = 2

Looping over indices is helpful sometimes when we have multiple lists that are related.

participant_id = ["001", "002", "003"]
age = [23, 34, 18]
for i in range(len(participant_id)):
    print(participant_id[i], age[i])
001 23
002 34
003 18

Another option in this case is to use the zip function, which “zips” together two (or more) lists so you can iterate over them together. At each part of the list, you get a tuple of the current element of the two lists.

for p_id, p_age in zip(participant_id, age):
    print(p_id, p_age)
001 23
002 34
003 18

Sometimes using zip results in code that is easier to read compared to using range.

Let’s look at looping using zip in more detail. If we run the following loop:

numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]
for n1, n2 in zip(numbers1, numbers2):
    print(n1, n2)

Each step of the loop accesses pairs of the items in the list:

numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]
n1, n2   =  ^

n1 = 1, n2 = 4

numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]
n1, n2   =     ^

n1 = 2, n2 = 5

numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6]
n1, n2   =        ^

n1 = 3, n2 = 6

Finally, sometimes it’s helpful to loop over a list while also keeping track of the current index in the list. To do this, we can use enumerate.

for i, p_id in enumerate(participant_id):
    print(p_id, age[i])
001 23
002 34
003 18

The enumerate function gives us the index first, then the corresponding element of the list.

Let’s look at using enumerate in more detail. If we run the following loop:

numbers = [1, 2, 3]
for i, n in enumerate(numbers):
    print(i, n)

Each step of the loop accesses an index of a list and that part of the list:

numbers = [1, 2, 3]
index   = [0, 1, 2]
i, n    =  ^

i = 0, n = 1

numbers = [1, 2, 3]
index   = [0, 1, 2]
i, n    =     ^

i = 1, n = 2

numbers = [1, 2, 3]
index   = [0, 1, 2]
i, n    =        ^

i = 2, n = 3

Exercise: types of for loops#

Write a list called letters with entries "a", "b", "c", and "d". Write another list called numbers with entries 1, 2, 3, and 4.

Write a loop using indexing (for i in range(len(letters))) and print out each letter.

Write another loop using zip and print out each letter with the corresponding number.

Write a final loop using enumerate and print out each index in letters with the corresponding letter at that index.

# answer here

Comprehensions#

In Python, comprehensions allow us to run some for loops in one line, when we just want to generate a new list or dictionary. We won’t use comprehensions much, but they’re used commonly enough in Python that they are good to know about.

List comprehensions#

A common use of for loops is to generate a list. In this case, we can instead use a list comprehension.

Before, we used a for loop to capitalize all the items in a list.

words = ["piano", "fountain", "hollow", "pupil"]
cap = []
for word in words:
    cap.append(word.capitalize())
print(cap)
['Piano', 'Fountain', 'Hollow', 'Pupil']

Python allows us to write the same thing in one line, using a list comprehension.

cap = [word.capitalize() for word in words]
print(cap)
['Piano', 'Fountain', 'Hollow', 'Pupil']

The first part (word.capitalize()) indicates how to define each item in the new list. The second part (for word in words) indicates what we’re looping over, and what each item in the loop should be called. Here, we set each word to the temporary variable word.

We can also add an if statement at the end to filter the input list. Only items that return True will be included.

cap_p = [word.capitalize() for word in words if word.startswith("p")]
print(cap_p)
['Piano', 'Pupil']

List comprehensions take a little getting used to, but can be useful. You can always write out a for loop instead though!

Dict comprehensions#

A similar method can be used to generate dictionaries using a dict comprehension. For example, say we want to create a dictionary with a key for each index in the list, and the value set to each item in the list.

d = {i: w for i, w in enumerate(words)}
print(d)
{0: 'piano', 1: 'fountain', 2: 'hollow', 3: 'pupil'}

To use this method, we need to generate both the key we want to use for each entry and the corresponding value. We then define each key, value pair to put in the dictionary.

While loops#

Besides for loops, there is also a different option of while loops, which can be used when we want to repeat something as long as some condition applies.

One example of using a while loop is when the user may want to do something multiple times, so that we don’t know in advance how many times we need to run a command. Here, this program prints whatever the user inputs, unless they type quit. Uncomment the lines before running it.

# while True:
#     s = input("Write a word, or 'quit' to stop.")
#     if s == "quit":
#         break
#     print(s)

The input function prints some prompt, then takes a string input from the user. The break statement breaks out of the loop. This can be used in for loops also, if we want to stop looping earlier, before running out of items to loop over.

Exceptions#

Sometimes a program is unable to run, often because of some problem with the inputs to that function. To deal with these cases, they can signal a problem by raising an exception.

When an exception is raised, it will halt execution of the program. This allows Python programs to stop if some assumption of the program has been violated.

For example, say we need to check that participant IDs are formatted correctly. We can use the .startswith string method to check if it starts with the correct "sub-" text. If not, then we raise an error.

participant_ids = ["sub-001", "sub-002", "sub-003"]
# participant_ids.append("subs-004")  # throws an error (try it!)
for pid in participant_ids:
    if not pid.startswith("sub-"):
        raise ValueError(f"ID does not start with 'sub-': {pid}")

There are many kinds of errors to signal different types of things going wrong. Two common ones are ValueError (there was some sort of unexpected value for a variable) and IOError (there was some problem with reading in a file).

We can let a program continue even if an exception is raised by using a try/except block.

letters = ["a", "b", 2]
cap = []
for letter in letters:
    try:
        cap.append(letter.capitalize())
    except:
        continue
print(cap)
['A', 'B']

This lets us “try” some code, see if there is an exception, and if so, run some “backup” code in case an exception has been raised. Here, we just use the continue keyword to skip to the next loop.

Exercise: exceptions#

Create a variable x = ["a", 1]. Try running sum(x). This will raise an exception because you can’t sum a letter and a number together. Write a try/except block that attempts to run sum(x) and prints "Error: cannot calculate sum" if an exception is raised.

# answer here

Example: putting control flow together#

Control flow becomes much more powerful when we use features together. Let’s go over a commonly used example of control flow: the FizzBuzz game. In this game, you loop over numbers from 1 to 15. For each number, follow these rules:

If a number is divisible by 3, print "Fizz".

If a number is divisible by 5, print "Buzz".

If a number is divisible by 3 and 5, print "FizzBuzz".

def fizzbuzz(n):
    for i in range(1, n + 1):
        if (i % 3 == 0) and (i % 5 == 0):
            print(i, "FizzBuzz")
        elif i % 3 == 0:
            print(i, "Fizz")
        elif i % 5 == 0:
            print(i, "Buzz")
fizzbuzz(15)
3 Fizz
5 Buzz
6 Fizz
9 Fizz
10 Buzz
12 Fizz
15 FizzBuzz

Example: using a counter#

Sometimes, it’s useful to have a counter that is updated during a for loop. For example, say we want to count how many times we get Fizz (a number divisible by 3) in a list of numbers. We first initialize the counter to zero (count = 0). Then, each time we find that the condition is met, we increment the counter. Here, we use a special syntax for adding to an existing variable, using +=. The count += 1 expression means the same thing as count = count + 1, but is easier to write and read.

def count_fizz(n):
    count = 0
    for i in range(1, n + 1):
        if i % 3 == 0:
            count += 1
    return count
print(count_fizz(15))
print(count_fizz(1000))
5
333

Example: using exceptions#

Sometimes, a function can encounter a problem that means it cannot run. We can raise an exception to deal with this kind of problem.

def count_fizz(n):
    if n < 1:
        raise ValueError("n cannot be less than 1")
    count = 0
    for i in range(1, n + 1):
        if i % 3 == 0:
            count += 1
    return count
# count_fizz(0)  # this will throw an error (try it!)

Example: using while loops#

Sometimes, we don’t just want to loop over a list. Sometimes we instead need to loop until some condition is met.

For example, say we want to find the first \(n\) times that we get Fizz and return a list of those times.

def list_first_fizz(n):
    found = []
    i = 1
    while len(found) < n:
        if i % 3 == 0:
            found.append(i)
        i += 1
    return found
list_first_fizz(5)
[3, 6, 9, 12, 15]

Summary#

Like all programming languages, Python has control flow commands determine what code to run at what times.

if/elif/else statements run different code depending on what conditions apply.

for loops iterate over elements and run some code for each element.

while loops run as long as some condition is met.

exceptions allow programs to quit if there is some problem that keeps them from being able to do their job.