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

If statements#

Sometimes, we need to decide whether to run a command or not, depending on the conditions. To do this, we can write an if statement:

if some_condition:
    code_to_run_if_true

The simplest form of an if statement is to run some code, only if a condition is satisfied. For example, say if we want to check if one variable is larger than another and run some code only if it is.

a = 5
b = 8
if a > b:
    print("a is greater than b.")

Because \(a < b\), the code is not executed.

We could instead check if the opposite is true.

if b > a:
    print("b is greater than a.")
b is greater than a.

Here, the code checks if \(b>a\), finds that it is, and runs the code to print our message.

We can optionally also include an else block at the end. The code here will run if the condition is not true.

if b > a:
    print("b is greater than a.")
else:
    print("b is not greater than a.")
b is greater than a.

Exercise: simple if statements#

Write a function called issmall that takes in a positive number and uses an if statement to test if it is less than 10. It should return True if the integer is less than 10 and return False otherwise. Test it with inputs 1 and 10.

Advanced#

Write a function that works the same way, without using an if statement.

# answer here

We can put multiple checks together using an if/elif statement. The elif keyword is short for “else if”. Different conditions are checked in order. If the first condition is true, then the code in that block will be executed. If the first condition is false, then the elif condition will be checked. You can have as many elif conditions as you want.

a = 5
b = 8
if a > b:
    print("a is greater than b.")
elif b > a:
    print("b is greater than a.")
b is greater than a.

Finally, we can optionally also have an else block at the end. This will run code only if none of the other conditions were met.

a = 5
b = 5
if a > b:
    print("a is greater than b.")
elif b > a:
    print("b is greater than a.")
else:
    print("a is equal to b.")
a is equal to b.

To summarize, the syntax for if/elif/else statements is:

if condition:
    code_to_run
elif other_condition:
    different_code
else:
    code_if_neither_condition_true

The various parts of an if/elif/else statement can take any expression that returns a boolean value (that is, True or False). You can get a boolean using tests of equality (using == to test if two variables have the same value, or != to test if they have different values) or inequalities (<, >, <=, >=).

number = 4
string = "name"
print(number < 4)
print(number <= 4)
print(string == "name")
print(string != "name")
False
True
True
False

Exercise: if statements#

Define a function called greet prints out a greeting. It should take one input called user. If the user is "Mark", print "Hi, Mark S.!. If the user is "Helena", print "Hello, Helly R."

Advanced#

Add an else block so that, if the user is anyone else, the function will use an f-string to print "Greetings, [user]."

# answer here

Sometimes we may want to combine tests to check for a more complicated set of conditions. We can combine tests using the keywords and and or.

The and keyword checks if two statements are both True.

print(1 == 1 and 2 > 1)  # both parts are True, so this returns True
print(1 == 1 and 2 < 1)  # the first part is True, but the second is False
True
False

The or keyword checks if either statement is True, or if both are True.

print(1 == 0 or 2 > 1)  # the second part is True, so this returns True
print(1 == 0 or 2 < 1)  # both parts are False, so this returns False
True
False

Finally, we can also use the keyword not to get the opposite of some conditional. Sometimes it is easier to check if something does not meet some condition than to check if it does.

For example, sometimes a variable will be initialized to None to indicate that it is undefined. We can check this using not.

def process_condition_code(condition):
    if condition is not None:
        print(f"Current condition is {condition}.")
    else:
        print("Current condition is undefined.")

process_condition_code(1)
process_condition_code(None)
Current condition is 1.
Current condition is undefined.

Let’s look at a common example of combining conditionals. For example, say we want to test if a number is between 5 and 10 (inclusive). We’ll write a function to run this check and try it with some different numbers.

def check_in_range(x, lower=5, upper=10):
    return x >= lower and x <= upper

print(check_in_range(4))
print(check_in_range(6))
print(check_in_range(11))
False
True
False

Note that we used keyword arguments for our function, so that we can change the range from the default if we want.

print(check_in_range(0.5, lower=0, upper=1))
print(check_in_range(1.5, lower=0, upper=1))
True
False

Note that we could have also written the same thing using if statements.

def check_in_range2(x, lower=5, upper=10):
    if x >= lower:
        if x <= upper:
            return True
        else:
            return False
    else:
        return False

print(check_in_range(4))
print(check_in_range(6))
print(check_in_range(11))
False
True
False

This also works, but is harder to read. In this case, it’s better to combine your tests into one statement, x >= lower and x <= upper.

Exercise: combining conditionals#

Write a function called out_of_range that takes in a number x and checks if it is not between 0 and 1. If \(0 \ge x \ge 1\), it should return False; otherwise, it should return True.

Advanced#

Add keyword arguments for lower and upper bounds, with 0 and 1 as the defaults. Change your function to use those bounds instead. Having variables like 0 and 1 specified directly in the code is known as hard coding. By having these be arguments to the function instead, your code becomes more flexible and less “hard coded”.

# answer here

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

Advanced: 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.

while True:
    s = input("Write a word, or 'quit' to stop.")
    if s == "quit":
        break
    print(s)
---------------------------------------------------------------------------
StdinNotImplementedError                  Traceback (most recent call last)
Cell In[28], line 2
      1 while True:
----> 2     s = input("Write a word, or 'quit' to stop.")
      3     if s == "quit":
      4         break

File /opt/hostedtoolcache/Python/3.12.10/x64/lib/python3.12/site-packages/ipykernel/kernelbase.py:1281, in Kernel.raw_input(self, prompt)
   1279 if not self._allow_stdin:
   1280     msg = "raw_input was called, but this frontend does not support input requests."
-> 1281     raise StdinNotImplementedError(msg)
   1282 return self._input_request(
   1283     str(prompt),
   1284     self._parent_ident["shell"],
   1285     self.get_parent("shell"),
   1286     password=False,
   1287 )

StdinNotImplementedError: raw_input was called, but this frontend does not support input requests.

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.