Table content of this notebook.

- Review relational and logical operators
- Review predicates
- Simple conditional examples
- Function bodies and conditional branches with multiple statements
- The
`pass`

statement and dropping`else`

- Nested and chained conditionals
- Exercise 1:
`letterGrade`

- Exercise 2:
`addArticle`

- Exercise 3:
`daysInMonth`

(Challenge yourself) - Exercise 4: extend
`letterGrade`

(Challenge yourself) - Improving the style of functions with conditionals
- Digging Deeper: Quirks with Relational Operators
- Digging Deeper: Quirks with Logical Operators; Truthy and Falsey

In lecture 6 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]:

In [2]:

```
8 != 10
```

Out[2]:

In [3]:

```
'bunny' < 'cat'
```

Out[3]:

In [4]:

```
'do' >= 'dog'
```

Out[4]:

In [5]:

```
'A' == 'a'
```

Out[5]:

In lecture 6 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]:

In [7]:

```
'a' > 'b' or 'b' < 'c'
```

Out[7]:

In [8]:

```
not 10 > 100 # Intepreted as not (10 > 100)
```

Out[8]:

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]:

In [11]:

```
isBoiling(100)
```

Out[11]:

In [12]:

```
isWaterLiquidAtTemp(72)
```

Out[12]:

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]:

In [15]:

```
absolute(111)
```

Out[15]:

In [16]:

```
classify(-17)
```

Out[16]:

In [17]:

```
classify(111)
```

Out[17]:

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)
```

In [20]:

```
doWhenTemperature(50)
```

Does `doWhenTemperature`

return anything?

In [21]:

```
print(doWhenTemperature(50))
```

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)
```

In [24]:

```
musicRecommender(False, False, True)
```

In [25]:

```
musicRecommender(False, True, False)
```

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)
```

In [28]:

```
categorize(-20)
```

`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)
```

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)
```

In [34]:

```
warnWhenTooFast2(40)
```

Below are two correct variants of the `abs`

absolute value 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))
```

In [36]:

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

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):
if age < 8:
return 'G'
else:
if age < 13:
return 'PG'
else:
if age < 18:
return 'PG-13'
else:
return 'R'
print(movieAge1(5), movieAge1(10), movieAge1(15), movieAge1(20))
```

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

In [38]:

```
def movieAge2(age):
if age < 13:
if age >= 8:
return 'PG'
else:
return 'G'
else:
if age >= 18:
return 'R'
else:
return 'PG-13'
print(movieAge2(5), movieAge2(10), movieAge2(15), movieAge2(20))
```

Python uses **chained** (**multibranch**) conditionals with `if`

, `elif`

s, and `else`

to execute exactly one of several branches.

In [39]:

```
def movieAge3(age):
if age < 8:
return 'G'
elif age < 13:
return 'PG'
elif age < 18:
return 'PG-13'
else:
return 'R'
print(movieAge3(5), movieAge3(10), movieAge3(15), movieAge3(20))
```

**Remember:** Only the **first** true branch 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'
print(movieAgeWrong(5), movieAgeWrong(10), movieAgeWrong(15), movieAgeWrong(20))
```

`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
# Your code here
# Solution 1: start with highest score
def letterGrade(score):
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
print(letterGrade(95), letterGrade(85), letterGrade(75), letterGrade(65), letterGrade(55))
# Solution 2: start with lowest score
def letterGrade(score):
if score < 60:
return 'F'
elif score < 70:
return 'D'
elif score < 80:
return 'C'
elif score < 90:
return 'B'
else:
return 'A'
print(letterGrade(95), letterGrade(85), letterGrade(75), letterGrade(65), letterGrade(55))
```

In [42]:

```
letterGrade(92) # answer should be 'A'
```

Out[42]:

In [43]:

```
letterGrade(87) # answer should be 'B'
```

Out[43]:

In [44]:

```
letterGrade(60) # answer should be 'D'
```

Out[44]:

`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 Lecture 6:

```
def isVowel(letter):
return 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 letter.lower() in 'aeiou'
# Define your addArticle function below
# Your code here
def addArticle(word):
"""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]:

In [47]:

```
addArticle('ant')
```

Out[47]:

`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
# 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]:

In [50]:

```
daysInMonth(8) # August
```

Out[50]:

In [51]:

```
daysInMonth(2) # February
```

Out[51]:

In [52]:

```
daysInMonth(13) # Error message
```

Out[52]:

`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
# 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)
```

Out[54]:

In [55]:

```
letterGradeAndMessage(58)
```

Out[55]:

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))
```

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))
```

**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]:

In [59]:

```
10 < '10'
```

In [ ]:

```
10000000 >= '0'
```

**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 [60]:

```
0 == False
```

Out[60]:

In [61]:

```
1 == True
```

Out[61]:

In [62]:

```
2 > True
```

Out[62]:

In [63]:

```
1 > True
```

Out[63]:

In [64]:

```
1 > False
```

Out[64]:

In [65]:

```
0 > False
```

Out[65]:

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

In Python, `not`

has surprising behavior when given a non-boolean operand:

In [66]:

```
not 111
```

Out[66]:

In [67]:

```
not 0
```

Out[67]:

In [68]:

```
not 'ab'
```

Out[68]:

In [69]:

```
not ''
```

Out[69]:

**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 [70]:

```
0 == False
```

Out[70]:

In [71]:

```
'' == False # Only 0 and 0.0 are considered equal to False
```

Out[71]:

In [72]:

```
1 == True
```

Out[72]:

In [73]:

```
17 == True # Only 1 and 1.0 are considered equal to True
```

Out[73]:

In [74]:

```
'abc' == True
```

Out[74]:

`and`

and `or`

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

In [75]:

```
111 or 230 # If first value is Truthy, return it; otherwise return second
```

Out[75]:

In [76]:

```
0 or 230
```

Out[76]:

In [77]:

```
0 or 0.0
```

Out[77]:

In [78]:

```
'cat' or 'dog'
```

Out[78]:

In [79]:

```
'' or 'dog'
```

Out[79]:

In [80]:

```
0 or ''
```

Out[80]:

In [81]:

```
111 and 230 # If first value is Falsey, return it; otherwise return second
```

Out[81]:

In [82]:

```
0 and 230
```

Out[82]:

In [83]:

```
0 and 0.0
```

Out[83]:

In [84]:

```
'cat' and 'dog'
```

Out[84]:

In [85]:

```
'' and 'dog'
```

Out[85]:

In [86]:

```
0 and ''
```

Out[86]:

The following definition of `isVowel`

doesn't work. Explain why!

In [87]:

```
def isVowelWrong(s):
low = s.lower()
return low == ('a' or 'e' or 'i' or 'o' or 'u')
```

In [88]:

```
isVowelWrong('a') # This works
```

Out[88]:

In [89]:

```
isVowelWrong('b') # This works
```

Out[89]:

In [90]:

```
isVowelWrong('e') # This doensn't work. Why?
```

Out[90]: