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 common pattern is to loop over every item in a list and create a new list by modifying that list. In this example, we’ll use the capitalize method of strings to convert a list of words from lower case to capitalized case.

word = "piano"
capitalized_word = word.capitalize()
print(word, capitalized_word)
piano Piano

First, we can test how this works using an example word. If we have the word stored in a string variable called word, we can use the capitalize method to create a capitalized version of the word.

We want to capitalize each word in the words list. First, let’s see what this would look like without a for loop. We would have to run the same command multiple times, with slight changes.

cap = []  # empty list that will hold all the capitalized words
cap.append(words[0].capitalize())  # capitalize the first word and add to the list
cap.append(words[1].capitalize())  # second word
cap.append(words[2].capitalize())  # third word
cap.append(words[3].capitalize())  # fourth word
print(cap)
['Piano', 'Fountain', 'Hollow', 'Pupil']

This works, but involves a lot of copying and pasting. If we want to process a longer list, it will quickly become impractical. Instead, let’s use a for loop.

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

The code using a for loop runs exactly the same code as the first version, but is much more succinct. We only have to write the cap.append(word.capitalize()) part once, and let the for loop handle the iterating part.

Let’s take another look at 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

Looping over indices and multiple lists#

Before, we looped over items in a list. Another common approach is to loop over indices of a list instead. Using the range function, we can loop from 0 to some maximum number.

for i in range(4):
    print(i)
0
1
2
3

The range function generates numbers from 0 to whatever number is input, not including the maximum number. So, range(4) gives us 0, 1, 2, 3. This is useful because range(4) gives us all the indices of a list of length 4.

The range function can be useful to generate all the indices of a list. First, we get the length of the list using the len function. Then we pass this into the range function to get all the integers from 0 to the length of the list, minus one.

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

Using conditionals with loops#

We can use if statements within loops to create more flexible programs.

Say we have a set of participants, their ages, and their scores from a memory test.

participant_ids = ["sub-001", "sub-002", "sub-003", "sub-004"]
ages = [19, 22, 31, 27]    # age of participants in years
scores = [82, 89, 65, 98]  # memory test score out of 100

If we want to calculate the mean score, we can do this using a for loop with a variable that keeps track of the total, which is then divided by the number of participants.

total_score = 0
for score in scores:
    total_score += score
mean_score = total_score / len(scores)
print(mean_score)
83.5

Often, however, we may want to look at subsets of a dataset. For example, we might want to get the total score for participants under 30 years. We can do this by combining a for loop with an if statement.

total_score = 0  # total score so far
n_included = 0   # number of included participants
for age, score in zip(ages, scores):
    if age < 30:
        total_score += score
        n_included += 1
mean_score_included = total_score / n_included
print(mean_score_included)
89.66666666666667

We can see that the score for under-30 participants (89.7) was higher compared to when all participants were included (83.5).

Exercise: using conditionals with for loops#

Create two lists called accuracy and included.

The accuracy list will indicate the fraction correct on a behavioral test, and should have these values: 0.85, 0.92, 0.25, 0.87.

The included list will indicate whether each corresponding test score should be included or not. One score is excluded because the participant did not follow directions. For each score, if it should be included, the corresonding value of included will be True, and if it should not be included, it will be False. The included variable should have these values: True, True, False, True, to indicate that the third score should be excluded.

Use a for loop with an if statement to calculate the mean accuracy for the included scores.

# 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. We won’t use comprehensions much, but they’re used commonly enough in Python that they are good to know about.

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!

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), TypeError (a variable had the wrong data type), and RuntimeError (there was some other unexpected problem).

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

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.

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