4  Logic and Flow Control

Learning Outcomes for this Lesson

Upon completion of this lesson you should:

  • …perform Boolean logic using logical operators
  • …use relational operators
  • …apply Boolean logic to collections
  • …use Boolean logic with if/else blocks to control execution

4.1 Boolean Logic

4.1.1 Logical Operators

Boolean variables (i.e. True and False) can be used in conjuction with Python’s built-in keywords and, or and not to evaluate logical statements, which can be used to control the flow of a program (i.e. if x is True do one thing, otherwise to something else). These keywords have a direct connection to the English words we already use for logic:

Here we use the and keyword to see if both are true, which they are not:

condition1 = True
condition2 = False
condition1 and condition2
False

Using the or keyword we can see if either are true, which is the case:

condition1 = True
condition2 = False
condition1 or condition2
True

And we can use the not word to essentially reverse the value of condition2. not condition2 is equivalent to saying not False, which means True.

condition1 = True
condition2 = False
1condition1 and not condition2
1
Note that the order of operations here applies the not to condition2, and then evalulates the and comparison. If we are ever unsure we can use parentheses (i.e. (not condition2)).
True

The interaction between and, or and not can be expressed more formally using a truth table:

Table 4.1: Truth table for the weather
cold windy cold and windy cold or windy (not cold) and windy not (cold and windy)
True True True True False False
True False False True False True
False True False True True True
False False False False False True
One or the other but not both

English does not have word for “one or the other but not both”. The closest might be “either”, but this could also be taken to mean both. Instead we had to create a new technical term: “exclusive or”, which is sometimes denoted as “xor”. Table 4.1 does not include any columns for this. This would be (cold and not windy) or (windy and not cold), which is quite verbose. Sadly, cold xor windy is not valid Python.

4.1.2 Relational Operators

4.1.2.1 Comparing Numbers

Probably the most releavant value of bools for engineers is comparing numerical values to some condition using relational comparisons such as “less than” (<) and “greater than” (>). The symbols we use for these were presented in Table 2.2, but they are repeated below for convenience:

The keyboard symbols used as relational operators
Symbol Operation
> Greater than
< Less than
>= Greater than or equal to
<= Less than or equal to
== Equal to
!= Not equal to

Consider the case where we have some value and some threshold limit which we want the value stay below:

value = 4.3
limit = 5.0
value < limit
True

We can also combine comparisons, such as ensuring our value is between an upper and lower limit:

value = 4.3
lower_limit = 2.0
upper_limit = 5.0
(value > lower_limit) and (value < upper_limit)
True

Using brackets is always a good idea to avoid mistakes and also make it easier to read later. However, Python allows for the following way to ensure a value is between limits, which matches how we would write this:

value = 1.3
lower_limit = 2.0
upper_limit = 5.0
lower_limit < value < upper_limit
False

And lastly, we can add an = sign to our < and > to get “less/more than or equal to” if the situation calls for it:

value = 2.0
lower_limit = 2.0
upper_limit = 5.0
lower_limit <= value <= upper_limit
True

4.1.3 Type Checking

Another useful application of relational comparisons is to determine the type of a variable. We have already seen that a given operator (i.e. +) means different things depending on the variables involved. When we write more complicated programs we will probably need to do some “type-checking” ourselves. This can be done as follows:

a = 1
1check = type(a) == int
print("The result is", check)
1
Here we have used int as a keyword instead of int() the function. We can think of int() as a function that converts a given value to the type of int.
The result is True
Foot Guns

Checking the type is quite important in Python because the freedom that Python provides, in terms of conversion between types, can also create problems if we are not careful. This is sometimes called a “foot gun” because it allows us to “shoot ourselves in the foot”, which is an expression for hurting yourself by accident.

4.2 If-Else Statements

4.2.1 Evaluating Single Conditions

Now that we have seen how to generate True and False values using conditional comparisons, we need a way to use them. All programming languages include if-else statements. In Python the syntax is as follows:

pH = 4.0
1if pH < 7.0:
2    print('The solution is acidic')
1
if is a built-in keyword
2
Remember that we must indent all code following a :
The solution is acidic

But what happens if the solution is basic?

pH = 8.0
if pH < 7.0:
    print('The solution is acidic')
1if pH > 7.0:
    print('The solution is basic')
1
Here we have added a second if to catch the case of basic pH.
The solution is basic

The above code is not very efficient because both statements will be evaluated even though only one of them can be true. Small inefficiencies like this can cause problems in 2 ways.

  1. They make it harder to understand the code. A reader would wonder why we are check both when only one can be true.
  2. The evaluation of the statement can be computationally expensive. We’ll explore this further below.

Remedying the inefficient code is so important that Python has a built-in keyword for this: elif.

pH = 8.0
if pH < 7.0:
    print('The solution is acidic')
1elif pH > 7.0:
    print('The solution is basic')
1
elif is another built-in keyword that is short for “else if”, where “else” means more like “otherwise”.
The solution is basic

In the above version, only one of the above print statements is executed. Importantly, if the first if statement is True, the second elif statement is not even checked. Python executes these statements in order and stops as soon as it finds a condition that is satisfied. This behavior is illustrated in Figure 4.1.

Two If-Statements in Series

An If followed by an Else-If
Figure 4.1: Schematic diagram showing the flow of logic in if and elif statements. The flow on the right is more efficient since it skips the second check if the first one is satisfied.

You have probably noticed that the above logic is incomplete. What if the solution is neutral?

pH = 7.0
if pH < 7.0:
    print('The solution is acidic')
elif pH > 7.0:  
    print('The solution is basic')
1else:
    print('The solution is neutral')
1
else is also a built-in keyword.
The solution is neutral

Note that the else statement does not actually check to see if the pH is 7.0. It just assumes that if the program reached this point then 7.0 is the only possibility remaining. This behavior is why writing a program is sometimes like solving a puzzle.

Let’s rewrite the logic to assume nothing:

pH = 7.0
if pH < 7.0:
    print('The solution is acidic')
elif pH > 7.0:
    print('The solution is basic')
elif pH == 7.0:  
    print('The solution is neutral')
else: 
    print('The pH value is invalid')
The solution is neutral

Example 4.1 (Determine the length of an arbitrary variable) Write a function which accepts any value and returns is length. Note that using len() will result in a error on data which are not collections, so we must catch this.

Solution

def length(x):
1    if type(x) == int:
2        L = 1
    elif type(x) == float:
        L = 1
    elif type(x) == bool:
        L = 1
    elif type(x) == list:
3        L = len(x)
    elif type(x) == dict:
        L = len(x)
    elif type(x) == tuple:
        L = len(x)
4    else:
5        L = 0
    return L

print("Length of 1 is:", length(1))
1
Here we start checking the data types which are not containers. Have we missed any?
2
We have assumed the length of a single value is 1.
3
If x is one of usual collection types the len() function works fine so we just use it.
4
This last statement catches everything else that was not covered above.
5
Since we assumed 1 length for single values, we’ll use 0 to indicate “unknown”. Incidentally, the length of an empty collection is also 0.
Length of 1 is: 1

Comments

There are several improvements we could make to the length() function. The main improvement would be to use the or keyword to combine several checks on the same line, which we’ll cover later in this lecture.

Another improvement is that Python lets us try something, then do something else if that fails. We’ll discuss this more when we talk about writing Pythonic code, but here is a sample:

try:
1    L = len(x)
except:
2    L = 0
return L
1
If this works then L gets a value and everything is happy.
2
If the try failed, then this block is called as a backup plan.

4.2.2 Using Conditions within Loops

4.2.2.1 Exiting a For-Loop

For-loops were introduced in the last chapter, but we will take another look. We can include if-statements inside for-loops:

temperature = [25.0, 25.6, 29.0, 31.9, 32.5, 28.0]  # degrees C
time = [0, 1, 2, 3, 4, 5]  # hours
for i in range(len(time)):
    if temperature[i] > 30.0:
        print('The temperature exceeded 30C at', time[i], 'hours')
The temperature exceeded 30C at 3 hours
The temperature exceeded 30C at 4 hours

We can also exit a for-loop early if some condition is met. For instance, if we want to find the first occurence of a value:

temperature = [25.0, 25.6, 29.0, 31.9, 32.5, 28.0]  # degrees C
time = [0, 1, 2, 3, 4, 5]  # hours
for i in range(len(time)):
    if temperature[i] > 30.0:
        t = time[i]
1        break
print("The temperature first exceeded 30C at", t, "hours")
1
break is another built-in keyword in Python. It does more or less what the work means: it breaks out of the loop and ends it.
The temperature first exceeded 30C at 3 hours

4.2.2.2 Introducing The While-Loop

While-loops have nearly the same purpose as for-loops except they can potentially run forever instead of iterating over a fixed set of items.

1i = 0
while i < 4:
    print(i)
2    i = i + 1
1
When using a while-loop, it can potentially run forever. We are iterating until we choose to stop it, rather than when we reach the end of a list. This means we usually must create our own counter, i in this case.
2
Note that we have to increment the counter ourselves! If you forget to increment this, the while-loop will literally run forever (or until the computer battery is empty). Note that you can do ctrl-c at the Python console to stop a running process.
0
1
2
3

We can also put the condition inside the while-loop to get more complicated logic:

keep_running = True
i = 0
j = 3
while keep_running:
    i = i + 1
    j = j + 1
    if (i > 5):
        keep_running = False
        print("i has exceeded the limit")
    elif (j > 5): 
        keep_running = False
        print("j has exceeded the limit")
j has exceeded the limit

You might also use a while-loop when the dataset you’re analyzing can change size during processing. This can happen if you are deleting items after they are processed, then you would stop with the length of the list is 0. This is explored in Example 4.2.

Example 4.2 (Using If-Statements to End a While Loop) Print the square root of each number in a list. Use a while loop, and only stop once the list is empty.

Solution

values = [4, 9, 16, 25, 36]
1keep_running = True
2while (keep_running is True):
3    val = values.pop(0)
    print("Result:", val**0.5) 
4    if len(values) == 0:
5        keep_running = False
1
We must initialize keep_running to True or else the while loop will never start
2
The while loop will process its contained logic as long as keep_running is True is itself True.
3
Here we use the pop method of the list which removes the requested item (0 in this case) and returns it to val
4
The length of the list is checked, and it is 0, then no more items are left to process.
5
Here we set keep_running to False which means the while loop will stop.
Result: 2.0
Result: 3.0
Result: 4.0
Result: 5.0
Result: 6.0

Comments

The statement keep_running is True will evaluate to True if keep_running is True. However, we could drop the is True part and just let the value fo keep_running inform the while loop if it should keep running or not, so while keep_running: is sufficient.

Single Line If-Statements

Python prides itself on being easy to read. Typically this means using as few lines as possible. It is possible to write simple if-statements on a single line as follows:

a = 5
result = True if a > 0 else False
print("Result:", result)
Result: True

In keeping with Python’s desire to be ‘readable’ this syntax is almost self explanatory. We will revisit this trick when we talk about writing Pythonic code.

4.2.3 Combining Conditions with and and or

Technically it is possible to have pH < 0 and pH > 14, it just means extremely acid or basic. Let’s add more categorizations to catch these:

pH = 17.0
1if (pH >= 0.0) and (pH < 7.0):
    print('The solution is acidic')
elif (pH > 7.0) and (pH <= 14.0):
    print('The solution is basic')
elif pH == 7.0:  
    print('The solution is neutral')
elif pH < 0: 
    print('The solution is extremely acidic')
elif pH > 14:
    print('The solution is extremely basic')
else:
2    print('This statement will never print')
1
Here we have used the and keyword to combine the results of both the bracketed statements. They both need to be True (i.e. True and True) for this check to be selected.
2
This statement will never actually be called since all possible cases are covered by the preceding checks. In more realistic situations it is not always obvious if all conditions are covered, so adding and else to catch unplanned conditions is good practice.
The solution is extremely basic

4.2.4 Evaluating Multiple Conditions in a Collection with all() and any()

The list of Python built-in functions (Table B.1) contains any() and all(). These are used for evaluating the conditions for an entire data container, instead of just a single value. Let’s take a closer look:

container = [True, True, False]
any_true = any(container)
print("Result:", any_true)
Result: True

At least one of the items in container is True, so any() evaluates to True.

container = [True, True, False]
all_true = all(container)
print("Result:", all_true)
Result: False

But not all of the items are True, so all() returns False.

Unfortunately, this does not work on a list of numerical values the way we might wish. all() checks if things are True, not on the numerical values. In fact, all numerical values other than 0 are considered True. (Remember that True -> 1 and False -> 0).

a = [1, 2, -4, 6]
all_true = all(a)
print("Result:", all_true)
Result: True
The concept of Truthiness

“Truthy” is a word that computer programmers made up to describe something that evaluates to True or False. An example is the fact that any nonzero number is intepreted as True. The None data type also has a “Truthiness” and is considered False. The empty string '' is also considered False.

So to use the all() and any() functions on a list of data we must first evaluate each item:

pHs = [1.0, 12.5, 2.8, 8.8]
1temp = pHs.copy()
2for i in range(len(pHs)):
3    pHs[i] = pHs[i] < 7
4all_true = all(pHs)
print("Result:", all_true)
1
We make a copy of pHs so that we have a place to record the True/False values
2
We scan through each location in pHs
3
We evaluate if the pH is acidic or not
4
Now we can check the entire container all at once using all()
Result: False

Note that there is no none() function. This can be accomplished using not any(). If even one item in a collection is True then any() will return True, then the not will reverse this to a False.

4.2.5 Confirming a Single Value is in a Collection

Because Python strives to be “readable” by humans, it offers several tricks or shortcuts. We have already seen the use of the in keyword when iterating over a collection in a for-loop. However, it can also be used as follows:

a = [1, 2, 5, 6]
check = 3 in a
print("Result:", check)
Result: False

The power of this is well illustrated by revisiting the function we wrote in Example 4.1. Using the in keyword we could reduce that rather verbose code to the following:

def length_v2(x):
    if type(x) in (int, float, bool, complex):
        L = 1
    elif type(x) in (list, dict, tuple, set, str):
        L = len(x)
    else:
        L = 0
    return L

Now we have checked for all the single data types and all the container types with just a few lines. And moreover, verbally reading this code makes it quite clear what is happening.

4.3 Excercises

Copy and paste the following code to a new .py file in Spyder and work through each cell until you get the desired result.

# %% Problem 1: Using Logical Operators

# Given the variables `a = True`, `b = False`, and `c = True`:
# a. Use the `and` operator to check if both `a` and `c` are True.
# b. Use the `or` operator to check if either `a` or `b` is True.
# c. Use the `not` operator to negate the value of `b`.
#
# Fill in the code below:

a = True
b = False
c = True

a_and_b = 
a_and_c = 
a_or_b = 
not_b = 

print("Result of a and b:", a_and_b)
print("Result of a and c:", a_and_c)
print("Result of a or b", a_or_b)
print("Result of not b:", a_or_b)


# %% Problem 2: Using Relational Operators

# Given the variables `x = 10` and `y = 20`:
# a. Use the `>` operator to check if `x` is greater than `y`.
# b. Use the `<=` operator to check if `x` is less than or equal to `y`.
# c. Use the `==` operator to check if `x` is equal to 10.
#
# Fill in the code below:

x = 10
y = 20

is_greater = 
is_less_equal = 
is_equal = 

print("Is x greater than y?", is_greater)
print("Is x less than or equal to y?", is_less_equal)
print("Is x equal to 10?", is_equal)


# %% Problem 3: Boolean Logic with Collections

# Given the list `numbers = [5, 8, 12, 16, 23]`:
# a. Use a loop and an `if` statement to check if all elements in the list are greater than 0.
# b. Use a loop to check if there is at least one element in the list that is divisible by 4.
#
# Hint: Use "break"
# Ex: 
# for ...
#   if ....
#       .....
#       break
#
# Fill in the code below:

numbers = [5, 8, 12, 16, 23]

# Check if all elements are greater than 0
all_positive = True
for num in numbers:
    pass

# Check if at least one element is divisible by 4
has_divisible_by_4 = False
for num in numbers:
    pass

print("Are all numbers positive?", all_positive)
print("Is there a number divisible by 4?", has_divisible_by_4)


# %% Problem 4: Using Boolean Logic in If/Else Blocks

# Write a program that asks the user for their age and checks:
# a. If the age is less than 18, print "Minor".
# b. If the age is between 18 and 65, print "Adult".
# c. If the age is 65 or older, print "Senior".
#
# Fill in the code below:

age = int(input("Enter your age: "))

if :
    pass
elif:
    pass
else:
    pass


# %% Problem 5: Logical Operators in Complex Conditions

# Given the variables `temperature = 25` and `is_raining = False`:
# a. Use an `if` statement to print "Nice weather" if the temperature is between 20 and 30 (inclusive) and it's not raining.
# b. Otherwise, print "Not a good day for a walk."
#
# Test to see that this works for different combinations of temperature and is_raining

temperature = int(input("What is the temperature in celsius?"))
is_raining = False

# write your code here


# %% Problem: Using Elif Statements
#
# Write a program that asks the user to input their exam score (0-100) and:
# a. If the score is 90 or above, print "A".
# b. If the score is between 80 and 89, print "B".
# c. If the score is between 70 and 79, print "C".
# d. If the score is between 60 and 69, print "D".
# e. If the score is below 60, print "F".
#
# Fill in the code below:

score = int(input("Enter your exam score (0-100): "))

if score >= 90:
    print("A")
elif:
    pass
elif:
    pass
elif:
    pass
else:
    pass

# %% Problem 7: Using `all()` and `any()` with Lists

# Given the list `marks = [70, 85, 90, 76, 65]`:
# a. Use the `all()` function to check if all marks are 60 or above.
# b. Use the `any()` function to check if any mark is 90 or above.
#
# Note: An example that checks if all marks are integers is shown
#
# Fill in the code below:

marks = [70, 85, 90, 76, 65]

all_ints = all(type(mark) == int for mark in marks)
all_passed = all()
has_top_score = any()

print("All marks are 60 or above:", all_passed)
print("There is a mark 90 or above:", has_top_score)


# %% Problem 8: Using While Loops with If Statements
#
# Write a program that repeatedly asks the user to input a number.
# a. If the number is negative, stop the loop and print "Loop stopped".
# b. If the number is positive, print the number and continue asking.
# 
# Fill in the code below:

while True:
    number = int(input("Enter a number (negative to stop): "))
    
    if number < 0:
        print("Loop stopped")
        break  # Exit the loop if the number is negative
    else:
        print("You entered:", number)