3. Booleans and Conditionals
# Booleans
Python has a type bool
which can take on one of two values: True
and False
.
x = True
print(x)
print(type(x))
2
3
Rather than putting True
or False
directly in our code, we usually get boolean values from boolean operators. These are operators that answer yes/no questions. We'll go through some of these operators below.
# Comparison Operations
Operation | Description | Operation | Description | |
---|---|---|---|---|
a == b | a equal to b | a != b | a not equal to b | |
a < b | a less than b | a > b | a greater than b | |
a <= b | a less than or equal to b | a >= b | a greater than or equal to b |
def can_run_for_president(age):
"""Can someone of the given age run for president in the US?"""
# The US Constitution says you must "have attained to the Age of thirty-five Years"
return age >= 35
print("Can a 19-year-old run for president?", can_run_for_president(19))
print("Can a 45-year-old run for president?", can_run_for_president(45))
2
3
4
5
6
7
Comparisons are a little bit clever...
3.0 == 3
But not too clever...
'3' == 3
Comparison operators can be combined with the arithmetic operators we've already seen to express a virtually limitless range of mathematical tests. For example, we can check if a number is odd by checking that the modulus with 2 returns 1:
def is_odd(n):
return (n % 2) == 1
print("Is 100 odd?", is_odd(100))
print("Is -1 odd?", is_odd(-1))
2
3
4
5
Remember to use ==
instead of =
when making comparisons. If you write n == 2
you are asking about the value of n. When you write n = 2
you are changing the value of n.
# Combining Boolean Values
Python provides operators to combine boolean values using the standard concepts of "and", "or", and "not". And in fact, the corresponding Python operators use just those words: and
, or
, and not
.
With these, we can make our can_run_for_president
function more accurate.
def can_run_for_president(age, is_natural_born_citizen):
"""Can someone of the given age and citizenship status run for president in the US?"""
# The US Constitution says you must be a natural born citizen *and* at least 35 years old
return is_natural_born_citizen and (age >= 35)
print(can_run_for_president(19, True))
print(can_run_for_president(55, False))
print(can_run_for_president(55, True))
2
3
4
5
6
7
8
Quick, can you guess the value of this expression?
True or True and False
Python has precedence rules that determine the order in which operations get evaluated in expressions like above. For example, and
has a higher precedence than or
, which is why the first expression above is True
. If we had evaluated it from left to right, we would have calculated True or True
first (which is True
), and then taken the and
of that result with False
, giving a final value of False
.
You could try to memorize the order of precedence (opens new window), but a safer bet is to just use liberal parentheses. Not only does this help prevent bugs, it makes your intentions clearer to anyone who reads your code.
# Conditionals
While useful enough in their own right, booleans really start to shine when combined with conditional statements, using the keywords if
, elif
, and else
.
def inspect(x):
if x == 0:
print(x, "is zero")
elif x > 0:
print(x, "is positive")
elif x < 0:
print(x, "is negative")
else:
print(x, "is unlike anything I've ever seen...")
inspect(0)
inspect(-15)
2
3
4
5
6
7
8
9
10
11
12
Python adopts the if
and else
often used in other languages; its more unique keyword is elif
, a contraction of "else if". In these conditional clauses, elif
and else
blocks are optional; additionally, you can include as many elif
statements as you would like.
def f(x):
if x > 0:
print("Only printed when x is positive; x =", x)
print("Also only printed when x is positive; x =", x)
print("Always printed, regardless of x's value; x =", x)
f(1)
f(0)
2
3
4
5
6
7
8
Note especially the use of colons (:
) and whitespace to denote separate blocks of code. This is similar to what happens when we define a function - the function header ends with :
, and the following line is indented with 4 spaces. All subsequent indented lines belong to the body of the function, until we encounter an unindented line, ending the function definition.
# Boolean conversion
We've seen int()
, which turns things into ints, and float()
, which turns things into floats, so you might not be surprised to hear that Python has a bool()
function which turns things into bools.
print(bool(1)) # all numbers are treated as true, except 0
print(bool(0))
print(bool("asf")) # all strings are treated as true, except the empty string ""
print(bool(""))
# Generally empty sequences (strings, lists, and other types we've yet to see like lists and tuples)
# are "falsey" and the rest are "truthy"
2
3
4
5
6
We can use non-boolean objects in if
conditions and other places where a boolean would be expected. Python will implicitly treat them as their corresponding boolean value:
print(0)
elif "spam":
print("spam")
2
3
# Conditional expressions (aka 'ternary')
Setting a variable to either of two values depending on some condition is a pretty common pattern.
def quiz_message(grade):
if grade < 50:
outcome = 'failed'
else:
outcome = 'passed'
print('You', outcome, 'the quiz with a grade of', grade)
quiz_message(80)
2
3
4
5
6
7
8
Python has a handy single-line 'conditional expression' syntax to simplify these cases:
def quiz_message(grade):
outcome = 'failed' if grade < 50 else 'passed'
print('You', outcome, 'the quiz with a grade of', grade)
quiz_message(45)
2
3
4
5
# Exercise: Booleans and Conditionals
Head over to the Exercises notebook (opens new window) to get some hands-on practice working with booleans and conditionals.
# 1.
Many programming languages have sign (opens new window) available as a built-in function. Python doesn't, but we can define our own!
In the cell below, define a function called sign
which takes a numerical argument and returns -1 if it's negative, 1 if it's positive, and 0 if it's 0.
# Your code goes here. Define a function called 'sign'
def sign(x):
if x > 0:
return 1
elif x < 0:
return -1
else:
return 0
q1.check()
2
3
4
5
6
7
8
9
10
# 2.
We've decided to add "logging" to our to_smash
function from the previous exercise (opens new window).
def to_smash(total_candies):
"""Return the number of leftover candies that must be smashed after distributing
the given number of candies evenly between 3 friends.
>>> to_smash(91)
1
"""
print("Splitting", total_candies, "candies")
return total_candies % 3
to_smash(91)
to_smash(1)
2
3
4
5
6
7
8
9
10
11
12
Modify the definition in the cell below to correct the grammar of our print statement. (If there's only one candy, we should use the singular "candy" instead of the plural "candies")
A straightforward (and totally fine) solution is to replace the original print call with:
if total_candies == 1:
print("Splitting 1 candy")
else:
print("Splitting", total_candies, "candies")
2
3
4
Here's a slightly more succinct solution using a conditional expression:
print("Splitting", total_candies, "candy" if total_candies == 1 else "candies")
# 3. 🌶️
In the main lesson we talked about deciding whether we're prepared for the weather. I said that I'm safe from today's weather if...
- I have an umbrella...
- or if the rain isn't too heavy and I have a hood...
- otherwise, I'm still fine unless it's raining and it's a workday
The function below uses our first attempt at turning this logic into a Python expression. I claimed that there was a bug in that code. Can you find it?
To prove that prepared_for_weather
is buggy, come up with a set of inputs where it returns the wrong answer.
def prepared_for_weather(have_umbrella, rain_level, have_hood, is_workday):
# Don't change this code. Our goal is just to find the bug, not fix it!
return have_umbrella or rain_level < 5 and have_hood or not rain_level > 0 and is_workday
# Change the values of these inputs so they represent a case where prepared_for_weather
# returns the wrong answer.
have_umbrella = True
rain_level = 0.0
have_hood = True
is_workday = True
# Check what the function returns given the current values of the variables above
actual = prepared_for_weather(have_umbrella, rain_level, have_hood, is_workday)
print(actual)
q3.check()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Solution: One example of a failing test case is:
have_umbrella = False
rain_level = 0.0
have_hood = False
is_workday = False
2
3
4
Clearly we're prepared for the weather in this case. It's not raining. Not only that, it's not a workday, so we don't even need to leave the house! But our function will return False on these inputs.
The key problem is that Python implictly parenthesizes the last part as:
(not (rain_level > 0)) and is_workday
Whereas what we were trying to express would look more like:
not (rain_level > 0 and is_workday)
# 4.
The function is_negative
below is implemented correctly - it returns True if the given number is negative and False otherwise.
However, it's more verbose than it needs to be. We can actually reduce the number of lines of code in this function by 75% while keeping the same behaviour.
See if you can come up with an equivalent body that uses just one line of code, and put it in the function concise_is_negative
. (HINT: you don't even need Python's ternary syntax)
def is_negative(number):
if number < 0:
return True
else:
return False
def concise_is_negative(number):
return number < 0 # Your code goes here (try to keep it to one line!)
q4.check()
2
3
4
5
6
7
8
9
10
# 5.
The boolean variables ketchup
, mustard
and onion
represent whether a customer wants a particular topping on their hot dog. We want to implement a number of boolean functions that correspond to some yes-or-no questions about the customer's order.
def wants_all_toppings(ketchup, mustard, onion):
"""Return whether the customer wants "the works" (all 3 toppings)
"""
return ketchup and mustard and onion
q5.a.check()
2
3
4
5
6
def wants_plain_hotdog(ketchup, mustard, onion):
"""Return whether the customer wants a plain hot dog with no toppings.
"""
return not (ketchup or mustard or onion)
q5.b.check()
2
3
4
5
6
def exactly_one_sauce(ketchup, mustard, onion):
"""Return whether the customer wants either ketchup or mustard, but not both.
(You may be familiar with this operation under the name "exclusive or")
"""
return (ketchup and not mustard) or (mustard and not ketchup)
q5.c.check()
2
3
4
5
6
7
# 6. 🌶️
We’ve seen that calling bool()
on an integer returns False
if it’s equal to 0 and True
otherwise. What happens if we call int()
on a bool? Try it out in the notebook cell below.
Can you take advantage of this to write a succinct function that corresponds to the English sentence "does the customer want exactly one topping?"?
def exactly_one_topping(ketchup, mustard, onion):
"""Return whether the customer wants exactly one of the three available toppings
on their hot dog.
"""
return (int(ketchup) + int(mustard) + int(onion)) == 1
q6.check()
2
3
4
5
6
7
Solution: This condition would be pretty complicated to express using just and
, or
and not
, but using boolean-to-integer conversion gives us this short solution:
return (int(ketchup) + int(mustard) + int(onion)) == 1
Fun fact: we don't technically need to call int
on the arguments. Just by doing addition with booleans, Python implicitly does the integer conversion. So we could also write...
return (ketchup + mustard + onion) == 1
# 7. 🌶️
In this problem we'll be working with a simplified version of blackjack (opens new window) (aka twenty-one). In this version there is one player (who you'll control) and a dealer. Play proceeds as follows:
- The player is dealt two face-up cards. The dealer is dealt one face-up card.
- The player may ask to be dealt another card ('hit') as many times as they wish. If the sum of their cards exceeds 21, they lose the round immediately.
- The dealer then deals additional cards to himself until either:
- the sum of the dealer's cards exceeds 21, in which case the player wins the round
- the sum of the dealer's cards is greater than or equal to 17. If the player's total is greater than the dealer's, the player wins. Otherwise, the dealer wins (even in case of a tie).
When calculating the sum of cards, Jack, Queen, and King count for 10. Aces can count as 1 or 11 (when referring to a player's "total" above, we mean the largest total that can be made without exceeding 21. So e.g. A+8 = 19, A+8+8 = 17)