1. Review relational and logical operators

1.1 Relational operators

In the previous lecture, we discussed relational operators. They are binary operators (take two operands, one on each side) and evaluate to a boolean value of True or False. We discussed six operators: >, >=, <, <=, ==, !=.

Predict the values of the following expressions:

In [1]:
7 > 9
Out[1]:
False
In [2]:
8 != 10
Out[2]:
True
In [3]:
'bunny' < 'cat'
Out[3]:
True
In [4]:
'do' >= 'dog'
Out[4]:
False
In [5]:
'A' == 'a'
Out[5]:
False

1.2 Logical operators

In the previous lecture, we also introduced three logical operators: and, or, and not. The first two are binary operators (they take two operands) while not is a unary operator (it takes only one operand). Usually, these operators take as their operands boolean values that are the output of relational expressions and predicates. For example:

In [6]:
10 > 5 and 10 < 20
Out[6]:
True
In [7]:
'a' > 'b' or 'b' < 'c'
Out[7]:
True
In [8]:
not 10 > 100 # Intepreted as not (10 > 100)
Out[8]:
True

2. Review predicates

Finally, in lecture 6 we also saw that a predicate is simply a function that returns a boolean value. Here are some sample predicates:

In [9]:
def isFreezing(temp):
    return temp <= 32

def isBoiling(deg):
    return deg >= 212

def isWaterLiquidAtTemp(t):
    return (not isFreezing(t)) and (not isBoiling(t))

Before running the examples below, try to first guess their output.

In [10]:
isFreezing(20)
Out[10]:
True
In [11]:
isBoiling(100)
Out[11]:
False
In [12]:
isWaterLiquidAtTemp(72)
Out[12]:
True
In [ ]:

3. Control Flow Diagrams and Conditional Statements

In programs, control is where the computer is currently executing code. Think of it as a "you are here" arrow in the code, much like a "sing this word" indicator in a karaoke song.

We imagine that control flows through a program, executing statements one-by-one along the way. This is visualized by control flow diagrams with arrows showing how control flows from one statement to another. For example, for the sequence of the three Python statements

ageString = input('Enter your age> ') # statement1
ageInt = int(ageString) # statement2
print('In ten years, you will be ' + str(ageInt + 10) + ' years old') #statement3

the control flow looks like this:

An if statement (also called a conditional statement) chooses between two branches based on a test value. Conditional statements can be understood as branching flow in control flow diagrams. The meaning of the if statement

if bool_expression:
    statement1
    statement2
    statement3
else: 
    statement4
    statement5

can be understood by the following control flow diagram:

Note that (1) only one of the two branches can be taken and (2) each branch can have multiple statements

In [13]:
def branchingFlow(boolVal):
    if boolVal:
        print("statement1")
        print("statement2")
        print("statement3")
    else: 
        print("statement4")
        print("statement5")
In [14]:
branchingFlow(True)
statement1
statement2
statement3
In [15]:
branchingFlow(False)
statement4
statement5

Either or both branches in a conditional statement may have return statements that cause a return from an enclosing function:

In [16]:
def branchingFlowReturn(boolVal):
    if boolVal:
        print("statement1")
        print("statement2")
        print("statement3")
        return('trueBranch')
    else: 
        print("statement4")
        print("statement5")
        return('falseBranch')
In [17]:
branchingFlowReturn(True)
statement1
statement2
statement3
Out[17]:
'trueBranch'
In [18]:
branchingFlowReturn(False)
statement4
statement5
Out[18]:
'falseBranch'

4. Simple conditional examples

In [19]:
def absolute(n):
    '''Return the absolute value of the number n'''
    if n >= 0:
        return n
    else:
        return -n
    
def classify(num):
    '''Return a string indicating whether num is negative or not.'''
    if num < 0:
        return 'negative'
    else:
        return 'nonnegative'
In [20]:
absolute(-17)
Out[20]:
17
In [21]:
absolute(111)
Out[21]:
111
In [22]:
classify(-17)
Out[22]:
'negative'
In [23]:
classify(111)
Out[23]:
'nonnegative'

A function with a conditional might print something.

In [24]:
def doWhenTemperature(temp):
    if temp <= 65:
        print("Put on a sweater or coat.")
    else:
        print("You can wear short sleeves today.")
In [25]:
doWhenTemperature(72)
You can wear short sleeves today.
In [26]:
doWhenTemperature(50)
Put on a sweater or coat.

Does doWhenTemperature return anything?

In [27]:
print(doWhenTemperature(50))
Put on a sweater or coat.
None
In [28]:
def doILikeMissyElliott(rap, hiphop, pop):
    """A predicate to determine whether you like Missy Elliott"""
    return rap or (hiphop and pop) # parentheses are unnecessary but used for clarity

def musicRecommender(rap, hiphop, pop):
    """Simple function that prints """
    if doILikeMissyElliott(rap, hiphop, pop):
        print("You should listen to Missy Elliott.")
    else:
        print("I would avoid Missy Elliott.")
In [29]:
musicRecommender(True, False, False)
You should listen to Missy Elliott.
In [30]:
musicRecommender(False, False, True)
I would avoid Missy Elliott.
In [31]:
musicRecommender(False, True, False)
I would avoid Missy Elliott.

5. Function bodies and conditional branches with multiple statements

Both function bodies and conditional branches can contain multiple statements. Because condtional statements are just one kind of statements, a sequence of statements can contain multiple conditional statements.

In [32]:
def categorize(num):
    '''This function has 3 statements in its body.
       They are executed from top to bottom, one after the other.
    '''
    print('Categorizing', num)
    if num % 2 == 0:
        print("It's even")
    else:
        print("It's odd")
    if num < 0: 
        '''This branch has 2 statements.'''
        print("It's negative")
        print("(That means it's less than zero)")
    else:
        print("It's nonnegative")
In [33]:
categorize(111)
Categorizing 111
It's odd
It's nonnegative
In [34]:
categorize(-20)
Categorizing -20
It's even
It's negative
(That means it's less than zero)

6. The pass statement and dropping else

When we don't want to do anything in a conditional branch, we use the special pass statement, which means "do nothing". (It's a syntax error to leave a branch blank.)

In [35]:
def warnWhenTooFast(speed):
    if speed > 55:
        print("Slow down! You're going too fast")
    else:
        pass # do nothing
In [36]:
warnWhenTooFast(75)
Slow down! You're going too fast
In [37]:
warnWhenTooFast(40)

It's OK to have an if statement without an else clause. In this case, the missing else clause is treated as if it were a pass statement.

In [38]:
def warnWhenTooFast2(speed):
    if speed > 55:
        print("Slow down! You're going too fast")
In [39]:
warnWhenTooFast2(75)
Slow down! You're going too fast
In [40]:
warnWhenTooFast2(40)

Below are two correct variants of the absolute function defined above. Explain why they work.

In [41]:
def abs2(n):
    '''returns the absolute value of n'''
    result = n
    if n < 0:
        result = -n 
    return result

print(abs2(-17), abs2(42))
17 42
In [42]:
def abs3(n):
    '''returns the absolute value of n'''
    if n < 0:
        return -n
    return n

print(abs3(-17), abs3(42))
17 42

7. Nested and chained conditionals

It often make sense to have a conditional statement nested inside the branch of another conditional.

Below we show variants of a function that returns the movie rating appropriate for a given age of movier goer. (If you want to learn more about film ratings, read this Wikipedia article.)

In [43]:
def movieAge1(age):
    """Returns the movie rating for the given age."""
    if age < 8:
        return 'G'
    else:
        if age < 13:
            return 'PG'
        else: 
            if age < 18:
                return 'PG-13'
            else:
                return 'R'
    
def test_movieAge1(age):
    print("age =", age, "; rating =", movieAge1(age))
    
test_movieAge1(5)
test_movieAge1(10)
test_movieAge1(15)
test_movieAge1(20)
age = 5 ; rating = G
age = 10 ; rating = PG
age = 15 ; rating = PG-13
age = 20 ; rating = R

It's possible to expression the same conditional logic with different nested conditionals:

In [44]:
def movieAge2(age):
    """Returns the movie rating for the given age."""
    if age < 13:
        if age >= 8:
            return 'PG'
        else:
            return 'G'
    else:
        if age >= 18:
            return 'R'
        else: 
            return 'PG-13'
    
def test_movieAge2(age):
    print("age =", age, "; rating =", movieAge2(age))
    
test_movieAge2(5)
test_movieAge2(10)
test_movieAge2(15)
test_movieAge2(20)
age = 5 ; rating = G
age = 10 ; rating = PG
age = 15 ; rating = PG-13
age = 20 ; rating = R

Python uses chained (multibranch) conditionals with if, elifs, and else to execute exactly one of several branches.

In [45]:
def movieAge3(age):
    """Returns the movie rating for the given age."""
    if age < 8:
        return 'G'
    elif age < 13:
        return 'PG'
    elif age < 18:
        return 'PG-13'
    else:
        return 'R'
    
def test_movieAge3(age):
    print("age =", age, "; rating =", movieAge3(age))
    
test_movieAge3(5)
test_movieAge3(10)
test_movieAge3(15)
test_movieAge3(20)
age = 5 ; rating = G
age = 10 ; rating = PG
age = 15 ; rating = PG-13
age = 20 ; rating = R

Chained conditionals can be understood by control flow diagrams:

Important: As seen in the diagram. only the first branch that evaluates to True will be executed.

Important: As shown in the following example, the order of chaining conditionals matters!

In [46]:
def movieAgeWrong(age):
    if age < 18:
        return 'PG-13'
    elif age < 13:
        return 'PG'
    elif age < 8:
        return 'G'
    else:
        return 'R'

def test_movieAgeWrong(age):
    print("age =", age, "; rating =", movieAgeWrong(age))
    
test_movieAgeWrong(5)
test_movieAgeWrong(10)
test_movieAgeWrong(15)
test_movieAgeWrong(20)
age = 5 ; rating = PG-13
age = 10 ; rating = PG-13
age = 15 ; rating = PG-13
age = 20 ; rating = R

8. Exercise: daysInMonth

Define a function named daysInMonth that takes a month (as an integer) as the argument, and returns
the number of days in it, assuming the year is not a leap year.

Assume 1 is January, 2 is February, ..., 12 is December. If the month does not fall
between 1 and 12, return an error message as a string.

Make the function as concise as possible (group months by days, don't write 12 separate if-else clauses).

In [47]:
# Define your daysInMonth function below
# Your code here

def daysInMonth(month):
    """Returns number of days in a month or a message of error."""
    if month == 2:
        return 28
    elif month < 1 or month > 12:
        return "This is not a valid month."
    elif month == 4 or month == 6 or month == 9 or month == 11:
        return 30
    else:
        return 31
In [48]:
daysInMonth(4)  # April
Out[48]:
30
In [49]:
daysInMonth(8)  # August
Out[49]:
31
In [50]:
daysInMonth(2)  # February
Out[50]:
28
In [51]:
daysInMonth(13) # Error message
Out[51]:
'This is not a valid month.'

9. Improving the style of functions with conditionals

Having seen conditional statements, you may be tempted to use them in predicates. But most predicates can be defined without conditionals by using combinations of relational and logical operators. For example, compare the complicated and simplifed functions below:

In [52]:
def isFreezingComplicated(temp):
    if temp <= 32:
        return True
    else:
        return False
    
def isFreezingSimplified(temp):
    return temp <= 32

print(isFreezingComplicated(20), isFreezingComplicated(72))
print(isFreezingSimplified(20), isFreezingSimplified(72))
True False
True False
In [53]:
def isPositiveEvenComplicated(num):
    if num > 0:
        if num % 2 == 0:
            return True
        return False
    return False

def isPositiveEvenSimplified(num):
    return num > 0 and num % 2 == 0

print(isPositiveEvenComplicated(42), isPositiveEvenComplicated(17), isPositiveEvenComplicated(-36))
print(isPositiveEvenSimplified(42), isPositiveEvenSimplified(17), isPositiveEvenSimplified(-36))
True False False
True False False

10. Digging Deeper: Quirks with Relational Operators

What happens if we compare numerical to string values?

In Python 3, numerical and string values are never equal. Furthermore, attempting to use >, >=, <=, or < will result in a TypeError.

Try to guess the outputs of these relational expressions before running the cells.

In [54]:
7 == '7'
Out[54]:
False
In [55]:
10 < '10'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[55], line 1
----> 1 10 < '10'

TypeError: '<' not supported between instances of 'int' and 'str'
In [56]:
10000000 >= '0'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[56], line 1
----> 1 10000000 >= '0'

TypeError: '>=' not supported between instances of 'int' and 'str'

What happens if we compare numerical to boolean values?

In Python 3, in comparisons involving numbers and booleans, False is considered a synonym for 0 and True is considered a synonym for 1.

In [57]:
0 == False
Out[57]:
True
In [58]:
1 == True
Out[58]:
True
In [59]:
2 > True
Out[59]:
True
In [60]:
1 > True
Out[60]:
False
In [61]:
1 > False
Out[61]:
True
In [62]:
0 > False
Out[62]:
False

What to take away from the above examples: Python 3 allows the comparison of values of different types in some contexts. In general, it's better to stick with comparing values of the same type.

11. Digging Deeper: Quirks with Logical Operators; Truthy and Falsey

In Python, not has surprising behavior when given a non-boolean operand:

In [63]:
not 111
Out[63]:
False
In [64]:
not 0
Out[64]:
True
In [65]:
not 'ab'
Out[65]:
False
In [66]:
not ''
Out[66]:
True

Truthy vs. Falsey Values:

What's going on in the above examples?

In Python, it turns out that in many contexts where a boolean is normally expected, 0 and 0.0 are treated like False and all other numbers are treated like True. So not 0 evaluates to True and not 111 evaluates to False.

Similarly, the empty string is treated like False and nonempty strings are treated like True. So not '' evaluates to True and not 'ab' evaluates to False.

In contexts where a boolean is normally expected, values that act like True are called Truthy and values that act like False are called Falsey. So 0, 0.0, '' and False are Falsey values, while all other numbers, strings, and booleans are Truthy.

Sadly, things are more complicated when it comes to testing equality:

In [67]:
0 == False
Out[67]:
True
In [68]:
'' == False # Only 0 and 0.0 are considered equal to False
Out[68]:
False
In [69]:
1 == True
Out[69]:
True
In [70]:
17 == True # Only 1 and 1.0 are considered equal to True
Out[70]:
False
In [71]:
'abc' == True
Out[71]:
False

and and or also behave in surprising ways when their operands are not booleans:

In [72]:
111 or 230 # If first value is Truthy, return it; otherwise return second
Out[72]:
111
In [73]:
0 or 230
Out[73]:
230
In [74]:
0 or 0.0
Out[74]:
0.0
In [75]:
'cat' or 'dog'
Out[75]:
'cat'
In [76]:
'' or 'dog'
Out[76]:
'dog'
In [77]:
0 or ''
Out[77]:
''
In [78]:
111 and 230 # If first value is Falsey, return it; otherwise return second
Out[78]:
230
In [79]:
0 and 230
Out[79]:
0
In [80]:
0 and 0.0
Out[80]:
0
In [81]:
'cat' and 'dog'
Out[81]:
'dog'
In [82]:
'' and 'dog'
Out[82]:
''
In [83]:
0 and ''
Out[83]:
0

The following definition of isVowel doesn't work. Explain why!

In [84]:
def isVowelWrong(s):
    low = s.lower()
    return low == ('a' or 'e' or 'i' or 'o' or 'u')
In [85]:
isVowelWrong('a') # This works
Out[85]:
True
In [86]:
isVowelWrong('b') # This works
Out[86]:
False
In [87]:
isVowelWrong('e') # This doensn't work. Why?
Out[87]:
False

Solution notes go here:

Because 'a' is Truthy, 'a' or 'e' evaluates to 'a'. Similarly, ('a' or 'e' or 'i' or 'o' or 'u') is equivalent to 'a', and return low == ('a' or 'e' or 'i' or 'o' or 'u') is equivalent to return low == 'a'!

12. Digging Deeper: Short-Circuit Evaluation with and and or

Not only does or return the value of its left operand if it is Truthy; in this case the right operand is not even evaluated! This is called short-circuit evaluation of or:

In [88]:
(2 < 3) or ((1/0) > 0) # No ZeroDivisionError occurs because left operand is Truthy and right operad is never evaluated
Out[88]:
True
In [89]:
42 or ((1/0) > 0)
Out[89]:
42
In [90]:
(2 > 3) or ((1/0) > 0) # ZeroDivisionError occurs because left operand is False and (1/0) is evaluated in right operand
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[90], line 1
----> 1 (2 > 3) or ((1/0) > 0) # ZeroDivisionError occurs because left operand is False and (1/0) is evaluated in right operand

ZeroDivisionError: division by zero

Similarly, if the left operand of and is Falsey, it is returned immediately without even evaluating the right operand.

In [91]:
(2 > 3) and ((1/0) > 0)
Out[91]:
False
In [92]:
0 and ((1/0) > 0)
Out[92]:
0
In [93]:
'' and ((1/0) > 0)
Out[93]:
''
In [94]:
(2 < 3) and ((1/0) > 0)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[94], line 1
----> 1 (2 < 3) and ((1/0) > 0)

ZeroDivisionError: division by zero
In [ ]: