## 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. Exercise 1: letterGrade¶

Define a function named letterGrade that takes one score (a number between 0 and 100) and returns a letter grade for the course.

Assume A >= 90, B >= 80, C >= 70, D >= 60, F < 60.

Remember the example above that shows that the order in which you compare the values matters!

In [41]:
# Define your letterGrade function below

if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'

if score < 60:
return 'F'
elif score < 70:
return 'D'
elif score < 80:
return 'C'
elif score < 90:
return 'B'
else:
return 'A'


A B C D F
A B C D F

In [42]:
letterGrade(92) # answer should be 'A'

Out[42]:
'A'
In [43]:
letterGrade(87) # answer should be 'B'

Out[43]:
'B'
In [44]:
letterGrade(60) # answer should be 'D'

Out[44]:
'D'

## 8. Exercise 2: addArticle¶

Define a function named addArticle that takes a string argument and returns a new
string with the correct article (a or an) added to the front of the argument.
Use the function isVowel that we created in the Booleans Lecture:

def isVowel(letter):
return len(letter) == 1 and letter.lower() in 'aeiou'

For the two examples below:

addArticle('cat') ---> 'a cat'
addArtitcle('ant') ---> 'an ant'

Note: if s is a string, then s[0] is the first letter of the string.

In [45]:
def isVowel(letter):
return len(letter) == 1 and letter.lower() in 'aeiou'

"""Add an article to a word based on first letter."""
if isVowel(word[0]):
return 'an ' + word
else:
return 'a ' + word

In [46]:
addArticle('cat')

Out[46]:
'a cat'
In [47]:
addArticle('ant')

Out[47]:
'an ant'

## 9. Exercise 3: daysInMonth (Challenge yourself)¶

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

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

## 10. Exercise 4: extend letterGrade (Challenge yourself)¶

Extend the function letterGrade you wrote in Exercise 1 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

"""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'

## 11. 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


## 12. 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.

## 13. 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