Skip to content

3.1 If, Elif, Else Clauses

In this note, I will discuss conditional statements, and how we can use them to control the execution of our program under specific conditions.

Booleans

Now it is time to introduce another variable type in programming. Just like int, str, and NoneType, we now have another type to analyze: bool (short for boolean). A bool is something that is true or false. There are really no other values that a bool can take on. However, other types can thought of as boolean-like types, for which we call them "truthy" or "falsey" values.

Truthy and Falsey Values

We assign truth values to each of the types we know so far, soon we will grow these items to include more types of objects. For now, I would remember these items:

  • For an int type, every single number except 0 is truthy. So, 0 is false, and -10 is true.

  • For a str type, only the empty string (written as "") is falsey, and every non-empty string is truthy. That means a string like " " is truthy since is it non-empty (there is a space between the quotes).

  • None is falsey

  • We can say the truth value explicitly with True and False. Of course, True is truthy and False if falsey.

  • We will encounter other types of objects, however, a general rule is that an item is falsey if it is the empty for of that object, otherwise it is truthy.

You can actually see if items are truthy and falsey by calling bool over the item you seek the truth of. For example, in your Python interpreter, you can write:

>>> bool(4)
True
>>> bool(None)
False
>>> bool("")
False
>>> bool(" ")
True
>>> bool(0)
False
>>> bool(True)
True

Truthy values pop-up every once in a while, and these truthy values are specific to the Python language.

When we look at the Scheme language, truthy values are slightly different. (In the language C, truthy values are also different, and the same is apparent in Java, Rust, Go, etc.). You'll have to briefly look at the coding language you use to see what is truthy and what is falsey. However, the above is what Python considers true and false.

Logical Operators and Short Circuiting

Sometimes, we rely on multiple conditions being truthy, and other times, we rely on only one of many possible conditions being truthy. Thus, logical operators are used to be the solution to this problem.

Python's logical operators are the English words and, or, and not. (In other languages, you might see it as &&, ||, and !, respectively). Logical statements in Python therefore reads like English.

Logically, A and B requires both conditions A and B to be true. We can chain them together such as A and B and C and D and E to require A, B, C, D, and E to be true. If any of them are falsey values, the whole logical statement is falsey.

A or B requires only one of the items to be truthy. A could be truthy and B could be falsey, it can appear vice versa, or A and B are truthy. Only when both entries are falsey does this logical operator become falsey. Like chaining ands together, we can chain or. A or B or C or D or E requires at least one of the 5 conditions to be truthy to be a truthy statement (there could be two, three, four, five truthy values, but as long as one of them are truthy, it doesn't really matter what the others are). It will be falsey only if all 5 conditions are falsey.

not simply switches the truthy value of the item it is applied to. not True is falsey, and not False is truthy.

However, there is something unique about logical operators. They actually return something (like a function) besides True and False.

Let's investigate this behavior. Look at these two statements:

>>> 0 or 5
5
>>> 0 and 5
0

It seems the or evaluated to the second item, and the and evaluated the first item. Let's also look at

>>> 5 or 0
5
>>> 5 and 0
0

Now the or returned the first item, and the and returned the second item. Also, they return numbers! Let's look at some combination of ints and strs and NoneTypes and bools:

>>> 5 and None and 3
>>> 3 or None or False
3
>>> None or 3 or 5
3
>>> None or False or 3
3

The first line evaluates to None, so you see nothing in the interpreter. But it seems like it returns a specific item.

I'll just tell you what is happening here. This is called short-circuiting, and what happens is that if we already know the result of the logical statement, why do we need to evaluate even further? For example, the evaluation of 5 and None and 3 is already known once we see the None, and the return of 3 or None or False is already known when we see 3. Here are the rules of short-circuiting

  • and: Return the first falsey value. Otherwise, return the last item.

  • or: Return the first truthy value. Otherwise, return the last item.

You can see that and is like a mirror of or. Let me also say something about not

  • not will change what ever it is being applied on to be the bool type True or False.

The reason for the not working this way instead of evaluating to the type's truth counterpart is that things like not 0 is nonsensical to find a counterpart. Is the counterpart 1, -1 , 100? Who knows? So we just change not 0 to True, and not "hello" to False.

Let's run through some more examples of short-circuiting: Try these out and check your answers with your Python terminal. I'll explain it below. Some of the statements will give you a ZeroDivisionError, so keep track of those too.

>>> 3 and 8 and 0
>>> 3 or 1/0
>>> "" or "." or ""
>>> False and None
>>> None or False
>>> not True and not False

We can combine both logical operators. You would need to make sure you simplify the expressions down as you see the parenthesis:

>>> 3 or (4 and 0)
>>> 3 and ((5 or 8) and (9 and None))
>>> 0 and (1/0 or 1/0 and 1/0)

After you have seen the solutions, read the solutions I wrote here:

  • 3 and 8 and 0 The rule here is to keep evaluating the next items until we reach the first falsey one. If all of them are truthy, we would then return the last item. We go through it one-by-one: 3 is truthy, 8 is truthy, 0 is falsey. This is the first falsey value, so we return 0.

  • 3 or 1/0 The rule here is to keep evaluating the next item until we reach the first truthy one. If all of them are falsey, return the last item. We just evaluate 3 since it is our first truthy value, so we return 3. Note that 1/0 does not get read since we knew the result of the logical statement before we actually evaluated 1/0.

  • "" or "." or "" I'll start speeding up now. Since we are working with or, the first truthy value is ".", so we return '"."'

  • False and None The first falsey value is False, so we return False.

  • None or False All values are falsey, so we need to return the last item: False

  • not True and not False. Each individual item becomes False and True. Since False is the first falsey value, it evaluates to False.

Here are the solutions to the more involved compound logical statements:

  • 3 or (4 and 0). We only read the first item to find the first truthy value, so it is 3. We don't even have to read the parenthetical expression.

  • 3 and ((5 or 8) and (9 and None)): 3 is truthy, now we need to evaluate the second item, which is (5 or 8) and (9 and None). The first item of this subexpression, we need to evaluate. 5 or 8 evaluates to 5, this is truthy, so we keep on going. 9 and None evaluates to None. Therefore, we are really trying to figure out 5 and None, which results in None. The main logical statement simplifies to 3 and None, which is None.

  • 0 and (1/0 or 1/0 and 1/0): 0 evaluates to falsey, so the whole statement results in 0. Again, we do not have to evaluate all the 1/0 conditions because short-circuiting makes us stop once we know the answer.

Relational Operators

Relational operators evaluate to True or False. These are the actual bool types, not some alternative truthy/falsey values. A short list includes:

  • < less than

  • > greater than

  • <= less than or equal to

  • >= greater than or less than

  • == equal. Note that it has to be double equals, since we already dedicated a single equals = to be variable assignment.

A statement like 1 == 2 evaluates to False, and not 1 == 2 evaluates to True.

Anatomy of if

Now it's time to discuss the syntax of selection using the if clause. Similar to a function, you need 3 parts:

  1. the word if to indicate you are using selection
  2. conditional statement(s). Here is where you use your condition making, truthy/falsey, short-circuity logic
  3. a suite to execute in the case that your condition is true. This can be things like assigning a value to a variable, returning a value, or even putting another if clause there.

In english, it might read as the following: "if are met, then . If are not met, skip the and continue with what is next."

A general form might look like:

if <condition(s)>:
    <do_the_following>

An example is checking whether a number is odd, the function I might implement could look like

def parity(n):
    """Returns 1 if n is odd, and 0 otherwise."""
    if n % 2 == 1:
        return 1
    return 0

A couple things to think about:

  • return fully stops the execution of your function. It will simply return the value that you put into the return statement. See how the above case has 2 different returns, but both do not get read; only one of them and then the function stops.

  • the piece of code must be indented so that Python knows what it should/should not run if the condition is true.

  • I will be adding docstrings to each functions from now on to detail what each function will be accomplishing in high-level terms. Documentation, in general, is really good to do because it allows you and others to understand your code.

if, elif, else Chains

Now there are other things we can do with selection. Sometimes, the selection depends on separated conditions, not all under one. For example, if a number is a multiple of 3, do this, otherwise if the number is a multiple of 5, do this other thing, otherwise, the catch-all case is to do this other thing. For this we introduce the words elif and else. elif is just like another if statement, and else is the catch-all case. The pedagogical example is the fizzbuzz function. I'll modify its implementation to be the following: returns 'fizz' if the input is divisble by 3, buzz if the input is divisible by 5, 'fizzbuzz' if the input is divisible by 3 and 5, and returns None if no other case applies. I implemented the function below

def fizzbuzz(n):
    """Returns 'fizz' if the n is divisible by 3, 'buzz' if n is divisible by 5,
    'fizzbuzz' if it is divisible by 3 and 5, and returns None for any other 
    case. """
    if n % 3 == 0 and n % 5 == 0:
        return "fizzbuzz"
    elif n % 3 == 0:
        return "fizz"
    elif n % 5 == 0:
        return "buzz"
    else:
        return None

Let me analyze this program: - First question, does the order of the conditions matter? What if I did the divisible by 3 case before I did the divisible by 3 and 5 case? Well remember, return stops execution of the function and returns the value. So, putting the divisible by 3 case before all the other cases might produce incorrect results.

  • In general, when chaining if, elif, and else statements, you want to start with the most specific case, and then work your way towards the most general.

When putting all three selection statements, you can only end up evaluating one of the s. I'll show you this by modifying our fizzbuzz specification to: print 'fizz' if the number is divisible by 3, 'buzz' if it is divisible by 5, 'fizzbuzz' if it is divisible by both 3 and 5, and prints None if no cases match; the function will return None. Again, I'll write it below.

def fizzbuzz(n):
    """Prints 'fizz' if the n is divisible by 3, 'buzz' if n is divisible by 5,
    'fizzbuzz' if it is divisible by 3 and 5, and prints None for any other 
    case. Returns None"""
    if n % 3 == 0 and n % 5 == 0:
        print("fizzbuzz")
    elif n % 3 == 0:
        return print("fizz")
    elif n % 5 == 0:
        return print("buzz")
    else:
        print(None)
    return

Again, let's analyze this function:

  • all inputs will go to the return statement at the very end. For example, if I were to input 3, I land on the case of printing 'fizz'. Once I finish that I go to the end of our chain, where I return. I could also just not write the return at all, or write return None explicitly. All cases return None.

  • The structure of chaining the selection makes it so only one is evaluated. If I were to, say write "if" for each case and then an else in the following way:

def fizzbuzz(n):
    """Prints 'fizz' if the n is divisible by 3, 'buzz' if n is divisible by 5,
    'fizzbuzz' if it is divisible by 3 and 5, and prints None for any other 
    case. Returns None"""
    if n % 3 == 0 and n % 5 == 0:
        print("fizzbuzz")
    if n % 3 == 0:
        return print("fizz")
    if n % 5 == 0:
        return print("buzz")
    else:
        print(None)
    return

it would be wrong. Let's say I inputted 15 All division cases pass, so a Python session like

>>> fizzbuzz(15)
fizzbuzz
fizz
buzz
>>>

will end up like the above. That is because we didn't chain any statements as we did with the elif and else.

There is actually an interesting way to do this using string concatenation which does only if. I'll modify the implementation a slight in that the catch-all case will print out the empty string.

def fizzbuzz(n):
    s = ""
    if n % 3 == 0:
        s += 'fizz'
    if n % 5 == 0:
        s += 'buzz'
    print(s)

See how the ifs being separated in this way allows us to check both separated conditions. If I were to use an elif in the second case, only one of the two would execute its .

Summary

In this note, we covered boolean logic, short-circuiting, relational operators, and the nuances among the if, elif, and else. These build the foundation of the idea of executing one's program with selection. In the next note, we will consider another way of control, known as iteration.

Go to next section: [[3.2 While Clauses]]

Get back to 3. Control or toc CS61A.