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

3. Simple conditional examples

An if statement (also called a conditional statement) chooses between two branches based on a test value.

In [13]:
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 [14]:
absolute(-17)
Out[14]:
17
In [15]:
absolute(111)
Out[15]:
111
In [16]:
classify(-17)
Out[16]:
'negative'
In [17]:
classify(111)
Out[17]:
'nonnegative'

A function with a conditional might print something.

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

Does doWhenTemperature return anything?

In [21]:
print(doWhenTemperature(50))
Put on a sweater or coat.
None
In [22]:
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 [23]:
musicRecommender(True, False, False)
You should listen to Missy Elliott.
In [24]:
musicRecommender(False, False, True)
I would avoid Missy Elliott.
In [25]:
musicRecommender(False, True, False)
I would avoid Missy Elliott.

4. Function bodies and conditional branches with multiple statements

In [26]:
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 [27]:
categorize(111)
Categorizing 111
It's odd
It's nonnegative
In [28]:
categorize(-20)
Categorizing -20
It's even
It's negative
(That means it's less than zero)

5. 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 [29]:
def warnWhenTooFast(speed):
    if speed > 55:
        print("Slow down! You're going too fast")
    else:
        pass # do nothing 
In [30]:
warnWhenTooFast(75)
Slow down! You're going too fast
In [31]:
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 [32]:
def warnWhenTooFast2(speed):
    if speed > 55:
        print("Slow down! You're going too fast")
In [33]:
warnWhenTooFast2(75)
Slow down! You're going too fast
In [34]:
warnWhenTooFast2(40)

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

In [35]:
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 [36]:
def abs3(n):
    '''returns the absolute value of n'''
    if n < 0:
        return -n
    return n

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

6. 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 [37]:
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 [38]:
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 [39]:
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

Remember: 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 [40]:
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

7. Challenge Exercise 1: 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 [48]:
# 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 [49]:
daysInMonth(4)  # April
Out[49]:
30
In [50]:
daysInMonth(8)  # August
Out[50]:
31
In [51]:
daysInMonth(2)  # February
Out[51]:
28
In [52]:
daysInMonth(13) # Error message
Out[52]:
'This is not a valid month.'

8. Challenge Exercise 2: extend letterGrade

Extend the function letterGrade from the weekly exercises to be a letterGradeAndMessage function that also prints a message:

  • For all passing grades, it will initially print a message You passed! and then return the grade letter.

  • For the failing grade F, it will print the message Unfortunately, you failed. and then return the letter 'F'.

You may use the print statement only twice!

In [53]:
# Define your letterGradeAndMessage function below
# Your code here

def letterGradeAndMessage(score):
    """Returns the letter grade for a given score. Prints a message
    too, based on pass/fail threshold.
    """
    if score >= 60:
        print("You Passed!")
        if score >= 90:
            return 'A'
        elif score >= 80:
            return 'B'
        elif score >= 70:
            return 'C'
        elif score >= 60:
            return 'D'     
    else:
        print("Unfortunately, you failed.")
        return 'F'
In [54]:
letterGradeAndMessage(85)
You Passed!
Out[54]:
'B'
In [55]:
letterGradeAndMessage(58)
Unfortunately, you failed.
Out[55]:
'F'

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 [56]:
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 [57]:
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 [58]:
7 == '7'
Out[58]:
False
In [59]:
10 < '10'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/cw/qb_x9khd29s8p36grg63jwgh0000gn/T/ipykernel_22738/3105290259.py in <module>
----> 1 10 < '10'

TypeError: '<' not supported between instances of 'int' and 'str'
In [60]:
10000000 >= '0'
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/cw/qb_x9khd29s8p36grg63jwgh0000gn/T/ipykernel_22738/4015734477.py in <module>
----> 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 [61]:
0 == False
Out[61]:
True
In [62]:
1 == True
Out[62]:
True
In [63]:
2 > True
Out[63]:
True
In [64]:
1 > True
Out[64]:
False
In [65]:
1 > False
Out[65]:
True
In [66]:
0 > False
Out[66]:
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 [67]:
not 111
Out[67]:
False
In [68]:
not 0
Out[68]:
True
In [69]:
not 'ab'
Out[69]:
False
In [70]:
not ''
Out[70]:
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 [71]:
0 == False
Out[71]:
True
In [72]:
'' == False # Only 0 and 0.0 are considered equal to False
Out[72]:
False
In [73]:
1 == True
Out[73]:
True
In [74]:
17 == True # Only 1 and 1.0 are considered equal to True
Out[74]:
False
In [75]:
'abc' == True
Out[75]:
False

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

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

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

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