Chapter 6: If-Statements

So far, the programs we've been writing have been fairly robotic. Each one defines an ordered set of statements that every program will go through, every single time. There aren't any deviations from the main path, so while the programs might be useful, there isn't much difference in the output of any single program. This chapter will introduce the if-statement, a technique for specifying different paths through code based on the program state. Perhaps a user will ask to run a certain part of your program by entering a specific command at an input prompt, or the program will fail to run if they don't enter the valid password. If-statements will grant you the ability to execute a new piece of code if certain conditions are met.

Decision making

A computer program is made up of a set of instructions that guide a computer from some starting state to an ending state. You might say that all of our programs so far have been simple stories with a linear plot. At first, we set up a starting state, then we might get some data from the user that changes something very slightly, and we reach the end in the same way we do every other time we run the code. Introducing an if-statement changes a linear story into a choose-your-own-adventure book. Programs get really interesting when we give users the ability to change the way the program works, allowing them to make decisions that guide our program to new states. We're going to look at how to tell Python how to make these decisions based on what the user says, and to act differently in different situations.

Here's a simple example of an if-statement in action.

num = int(input("Enter a number: "))
if num > 0:
    print("That's a positive number!")

Enter a number: 42
That's a positive number!

Enter a number: -5

An if-statement includes a boolean expression called a test condition. If the result of the test condition is True, we execute a given block of code. If the test condition is False, the block is ignored. Our example above is a simple check for whether or not a number was greater than zero. We can evaluate this in the interpreter to see how Python will treat the check.

>>> num = 42
>>> num > 0
True
>>> num = -5
>>> num > 0
False

The important part of this example is the boolean values that are returned when using the greater-than operator. The same type of result occurs when you use other similar operators, such as the equality symbol. We could have substituted num == 0 instead of num > 0, and the meaning of the if-statement would change appropriately. A boolean value would be returned for the if-statement to evaluate.

You might be wondering how Python actually knew what code to run when the if-statement test condition was True. At what point does the specified block of code actually end? Python uses indentation to define the boundaries of a section of code that needs to be executed. For example,

num = int(input("Enter a number: "))
if num > 0:
    print("That's a positive number!")
    print("In fact, it's greater than zero.")
    print("Your number is {0}.".format(num))
print("All done!")

Enter a number: 42
That's a positive number!
In fact, it's greater than zero.
Your number is 42.
All done!

Enter a number: -5
All done!

A block of code is a section of one or more lines that are indented to the same level. In the example above, there are three lines immediately following the if-statement that are indented by four spaces. Python treats these three lines as a block of code that is only executed when the if-statement is True. When we revert back to the previous indentation level, as seen in the "All done!", the block is done. If num is not greater than zero, the indented block is skipped outright, and we move down to the "All done!" line immediately.

What if we wanted to add some functionality to tell the user when the number was negative, or even exactly equal to zero? The straightforward approach is to use several if-statements. At this point, it's a fair way to do things. Let's see how that would look.

num = int(input("Enter a number: "))
if num > 0:
    print("That's a positive number!")
if num < 0:
    print("That number is negative.")
if num == 0:
    print("Your number is exactly equal to zero!")
print("All done!")

Enter a number: 42
That's a positive number!
All done!

Enter a number: -5
That number is negative.
All done!

Enter a number: 0
Your number is exactly equal to zero!
All done!

Great! We've got several if-statements that are all checking various possibilities for the num variable, and any int value that the user can enter is caught by one of these checks. In every case, we hit the end of the program and output our "All done!" string.

When an if-statement doesn't evaluate to True, it necessarily evaluates to False. If num is not greater than zero, it is less than or equal to zero. This sounds obvious, but it's really important to drive home the point that a boolean value is either True or False. This leads to the else-statement, a keyword that can provide a corresponding block of code for the times when the if-statement doesn't give you a True value.

num = int(input("Enter a number: "))
if num > 0:
    print("That's a positive number!")
else:
    print("Your number is not positive!")
print("All done!")

Enter a number: 42
That's a positive number!
All done!

Enter a number: -5
Your number is not positive!
All done!

What the else-block provides is a set of code to run when the test condition in the corresponding if-statement evaluates to False. We could have written the following code instead:

num = int(input("Enter a number: "))
if num > 0:
    print("That's a positive number!")
if num <= 0:
    print("Your number is not positive!")
print("All done!")

In this case, it does exactly the same thing as the code that uses the else-statement. However, if we want to change the variable name, or give it a different condition, we also need to change all of the other if-statements that are making similar checks. The else-statement just works in conjunction with the if-statement to say "If something is true, do this code, otherwise, do that code." The else-statement is the "otherwise", and is a catch-all for anything that didn't fit with the original if-statement.

How could we use else to do our three-part example from above, where num is either positive, negative, or exactly equal to zero? Well, one approach is to use a technique called nesting to place if-statements inside of each other.

num = int(input("Enter a number: "))
if num > 0:
    print("That's a positive number!")
else:
    if num < 0:
        print("That number is negative.")
    else:
        print("Your number is exactly equal to zero!")
print("All done!")

That's fine, but it's a little verbose. Do we really need to write that much code to accomplish this task? And look at all of that indentation! If this keeps up, we're going to need a bigger screen.

Python has another statement that combines the if-statement and else-statement into a single statement called elif (else-if). An elif-statement combines the else and if keywords together into a check that occurs only if the original if-statement evaluated to False. We can rewrite the previous example to see how similar it looks, and how it saves us an entire additional level of indentation.

num = int(input("Enter a number: "))
if num > 0:
    print("That's a positive number!")
elif num < 0:
    print("That number is negative.")
else:
    print("Your number is exactly equal to zero!")
print("All done!")

The elif-statement does exactly the same thing that the nested code did, with even less indentation and extra writing. It only gets evaluated if the first check is False, and can even include an else statement at the bottom. We can even have multiple elif-statements. With this in mind, it should be clarified that elif-statments are only examined if all of the previous if or elif checks in the current if-statement have already evaluated to False. The else part of elif still means "only consider this code if you really haven't done anything yet!"

It's perfectly fine to write code like this:

num = int(input("Enter a number: "))
if num > 0:
    print("That's a positive number!")
elif num == 0:
    print("Your number is exactly equal to zero!")
elif num == -1:
    print("Negative one? Whoa.")
elif num == -2:
    print("That is a negative two.")
else:
    print("That number is negative.")
print("All done!")

There are a few special cases in that code where our elif-statement will print something interesting, and we still have the catch-all else statement at the bottom to get everything that manages to sneak through all the other checks above.

For an example of using if-statements, let's imagine that you're writing a program that should only be usable by someone who knows the secret password. For the sake of this example, we'll have to also imagine that they don't have access to your source code, but that's not unreasonable for the moment. What we'd like to do is present the user with an input prompt asking for the password. If they give an invalid value, we exit the program, and if they give the correct password, we let them in.

password = "spam"
user_password = input("Enter the password: ")
if password == user_password:
    print("SECRET ACCESS GRANTED.")
else:
    print("That's not the password! I can't let you in.")

Enter the password: turkey
That's not the password! I can't let you in.

Enter the password: spam
SECRET ACCESS GRANTED.

At the top of the code, we enter in the password we'll be looking for from the user. This can be any string, and in this case we've decided on a fairly simple one. (Alright, it's my password. Keep it secret, okay?) The second line asks the user to enter in the password to access the system. Now the real meat of the program comes in on the third line, where an if-statement is used to compare whether or not the user's password matches the password we're expecting. If it works, we grant access to the secret stuff. If not, output a message explaining that they're not allowed to come in.

Expressions

Think back to the definition of a boolean, and how a boolean value is either True or False. We used these in if-statements and elif-statements in the previous section, and referenced them as if it was the if-statement or elif-statement itself that was returning False. In actuality, it's a little more subtle than that. What we're really looking at is the expression contained in the statements themselves. Consider this code:

num = 42
print("Your number is {0}.".format(num))
if num > 0:
    print("That's a positive number!")
else:
    print("That number is negative.")

Your number is 42.
That's a positive number!

It was suggested previously that the if-statement itself was True. If you go to the interpreter and type in the actual check that's being performed, you can see what the truth check actually gives.

>>> num > 0
True

The component of an if-statement or elif-statement that is actually evaluated for truth is called an expression. It uses some values or variables to evaluate a boolean value, which then tells Python whether or not to execute the associated block of code. If the expression returns True, the code is evaluated, and if the expression returns False, the code is skipped over. To show this in code, consider the following:

if True:
    print("With True: Success!")
else:
    print("With True: Failure!")

With True: Success!

if False:
    print("With False: Success!")
else:
    print("With False: Failure!")

With False: Failure!

There are two if-statements with associated else-statements, and the expressions are stripped down to the bare boolean values. The output shouldn't surprise you, but it should serve as an illustrative example of what Python does with the expression part of an if-statement. If there are variables used in the expression, such as num > 0, Python attempts to evaluate the expression to determine the True or False value returned.

An expression is a combination of operators and values or variables, organized to always return a True or False value. In the case of if-statements, the code is set up so that the interesting block only gets executed if an interesting condition is True. In the following example, we see that an expression isn't limited to a single variable, or even a single comparison.

name = input("Enter your name: ")
age = int(input("Enter your age: "))
if name == "Alexander":
    print("Hello, me!")
elif age < 18:
    print("Programming at an early age.. Nice!")
else:
    print("Pleasure to meet you!")

Each check is done in an ordered way in isolation from one another. In the example, the first check that is done looks to see whether or not name is equal to "Alexander". If it is, the following indented block of code is executed, and the other elif and else blocks are ignored entirely. If the string comparison expression evaluates to False, the elif expression will be evaluated. This check doesn't need to involve name in any way. It happens to be used in the if-statement, but any other check can be performed, whether with name or not.

Python also has operators that make use of logic to chain smaller expressions into larger ones. The and and or keywords allow expressions to be combined together to evaluate complicated expressions. It may be convenient to think of the and and or keywords as glue that can hold smaller expressions together. For example, consider the following:

age = int(input("Enter your age: "))
if age >= 0 and age <= 150:
    print("Alright, you're {0} years old.".format(age))
else:
    print("I think you're trying to trick me.")

age = int(input("Enter your age: "))
if age < 0 or age > 150:
    print("I think you're trying to trick me.")
else:
    print("Alright, you're {0} years old.".format(age))

Enter your age: -5
I think you're trying to trick me.

There are two equivalent examples that use and and or to reach the same conclusion. In this case, we're testing the age variable that the user provides to see if it fits inside some acceptable range of valid ages. Your range might differ, and it's certainly true that few 1-year olds are probably using programs like this, but the point should be clear. (Also, if you're 150 and programming, what's your secret? Hook me up!) We could have an if-statement that checks if age is too high, and an associated elif-statement that checks if age is too low, followed by the else-statement stating that everything is fine. What we want to do is to check whether the user is trying to trick us or not. If the age is too low or the age is too high, print an error.

The reason for suggesting that and and or are like glue is that they need valid expressions on either side to be well formed. Many new programmers will read these operators as applying to the values themselves, and will try to write code like this:

if age >= 0 or <= 150:
    print("Alright, you're {0} years old.".format(age))

You can try to run that code, but Python will complain. Go ahead, give it a shot! This book is all about finding creative ways to break stuff, so feel free.

You must include the variable name before the operator, and each side of the or keyword should be able to evaluate in the interpreter. The correct way to write this expression is:

if age >= 0 or age <= 0:

If it helps, you have the option of including round brackets around the expressions themselves. Python uses this in the same way that mathematics does, where you can ensure that certain operations work in a particular way that might not be obvious at first glance. For example,

>>> 1 * 2 / 3 * 4
2.6666666666666665

A quick look at the top example might lead you to assume that we'd get the result of 1 * 2, then the result of 3 * 4, and that we'd use those results when dividing. In actuality, all of these multiplication and division operations have the same importance in Python, so it works along the expression from left to right. We can use the round brackets to show how the evaluator processes the data.

>>> 1 * 2 / 3 * 4
2.6666666666666665
>>> (((1 * 2) / 3) * 4)
2.6666666666666665

>>> (1 * 2) / (3 * 4)
0.16666666666666666
>>> (2) / (12)
0.16666666666666666

We can also use them to force Python to evaluate the multiplications first, identifying the numerator (1 * 2) and the denominator (3 * 4).

The same approach can be taken with expressions in our if-statements, and depending on your comfort level with more complicated expressions, it might help you to read the code better.

if age >= 0 or age <= 150:
    print("Alright, you're {0} years old.".format(age))

if (age >= 0) or (age <= 150):
    print("Alright, you're {0} years old.".format(age))

Both of these expressions evaluate to the same result, as the or operator expects an expression on either side of it. Since each side must evaluate to a real expression on its own, you can use the round brackets to visually identify the boundaries of each expression in this way.

The priority of operation for these commands is referred to as operator precedence. Python has a strictly defined order for the priority. For example, multiplication will always occur before addition or subtraction when possible. This is a familiar concept in mathematics, and it is expanded here thanks to the large number of operations available in the language.

>>> 2 * 3 + 4
10
>>> (2 * 3) + 4
10
>>> 2 * (3 + 4)
14
>>> 2 + 3 * 4
14
>>> (2 + 3) * 4
20
>>> 2 + (3 * 4)
14

Multiplication has a higher operator precedence than addition, and it doesn't matter at what character position in the expression it occurs. If there is an addition occurring to the left of a multiplication, as in the case of 2 + 3 * 4, the multiplication will necessarily be evaluated first. You can see this in the examples above. When the expression reads "two plus three times four," the "three times four" is evaluated first because multiplication has a higher operator precedence.

A brief table with some of the operators we've seen so far will help show the order in which Python evaluates its expressions. This list is not complete (there are a lot of operators in the language!), but it should cover the likely candidates that you're going to be using for now. It is sorted from lowest precedence to highest precedence, so the operations at the bottom of the list are evaluated first, and the ones at the top of the list are evaluated last.

Operator                    Description
or                          Boolean OR
and                         Boolean AND
not                         Boolean NOT
<, <=, >, >=, !=, ==        Comparisons
+, -                        Addition and subtraction
*, /                        Multiplication and division
**                          Exponent (to the power of)

Breaking Stuff

Let's go back to indexes for a moment. The zero-based counting system is a fundamental part of computing, and of software development in particular. When talking about the zeroth element, or the character at position 0 in a string, we make a reference to the first element or character in the object. But one specific case warrants a bit of extra attention. Let's use this simple example:

>>> my_string = "Alexander Coder"
>>> my_string.find("Alexander")
0
>>> my_string.find("Coder")
10
>>> my_string.find("Tomato")
-1

That's unsurprising, of course. My first and last names are found in my_string, and "Tomato" is not. What might be surprising, at least at first glance, is the following code. I say this specifically because I still fall into this trap from time to time. That's right, I'm not afraid to admit it.

my_string = "Alexander Coder"
if my_string.find("Alexander"):
    print("Hi, Alexander!")
else:
    print("Who are you?")

Who are you?

Did that code just tell me that "Alexander Coder" doesn't contain the string "Alexander"? Well, not exactly, but it kind of looks like it did. I asked the find function to give me the index in my_string where the substring "Alexander" begins, and the if-statement forced the else code block to run. If you're not careful, functions like find can be misunderstood as returning boolean values. In fact, since Python attempts to convert int values to bool ones when you perform an equality test in this way, the int value will actually get converted to a bool. The find function returns 0, as "Alexander" is found in my_string starting at the character at index 0. If "Alexander" was not found in the string, find would return -1.

So what the heck does converting an int to a bool actually mean? What numbers are False, and what numbers are True? Does 37 equal True? What about converting bool values to int values? There's a really subtle but important distinction that you're going to want to be aware of, especially when using functions like find.

>>> int(True)
1
>>> int(False)
0

For Python, the bool value True is equal to the int value 1, and the bool value False is equal to the int value 0. If you convert True or False to an int, those are the values that you'll get. We can verify this at the console, like this.

>>> True == 1
True
>>> False == 0
True

Python says that True and 1 are equal to each other, and that False and 0 are equal, just as we'd expect. What about converting int values to bool values? Is the opposite also valid?

>>> bool(1)
True
>>> bool(0)
False

Aha, just as we'd expect. The boolean result after conversion for the integer value 1 is True, as we'd expect. No surprises from False either, so we're golden, right? .. right?

A boolean variable can have one of two possible values. That's critically important. A boolean is True, or it's False. There is no other option. So what happens when we want to convert another int to a bool? What is the boolean representation of 2?

>>> bool(2)
True
>>> 2 == True
False

Now you wait just a second, Python. What on Earth is this? Python has converted the int value 2 into the bool value True. It has also told us that 2 and True are not equal to each other. Why, Python? Why? Let's reword the question in terms of int and float values.

>>> int(2.5)
2
>>> 2.5 == 2
False

The boolean representation of the number 2 is an approximation in the same way that the integer value 2 is an approximation of the floating point value 2.5. As it turns out, any number that isn't 0 will be True.

>>> for x in range(-5, 5):
             print(x, bool(x))

-5 True
-4 True
-3 True
-2 True
-1 True
0 False
1 True
2 True
3 True
4 True

So what does this mean? It means that you've got to be extremely careful when writing your if-statements. Yes, this is a long winded way of advising you to never write the following piece of code:

x = "Alexander"
if x.find("Alexander"):
    print("Found it!")
else:
    print("Didn't find anything!")

Didn't find anything!

That code seems to read like a perfectly reasonable request to see if the string x contains the value "Alexander". In fact, I look at that now and assume that any reasonable Python interpreter would do the sensible thing and tell me that it found the string. However, we must recall what the find function is designed to do. The find function identifies the position of the string you're looking for, and if your value starts with the search string, like x does in this example, you'll get position 0 returned. The if-statement converts that 0 into a boolean, and you're left with something like this:

if 0:
    print("Found it!")

In boolean, this is:

if False:
    print("Found it!")

Of course, if x doesn't start with the string,

if x.find("ott"):
    print("Found it!")
else:
    print("Didn't find anything!")

Found it!

Watch out for your implicit conversion inside of if-statements, as these are the type of errors that Python won't throw up a big red exception about. These are the really subtle pull-your-hair-out bugs. Don't forget that integer to boolean conversions in if-statements can cause strange side-effects, and always remember to add in explicit type equality testing if you think something like this might pop up!

x = "Alexander"
if x.find("Alexander") > -1:
    print("Found it!")
else:
    print("Didn't find anything!")

Found it!

Ahh, that feels better.

Summary

The introduction of if-statements is a critically important piece of the programming puzzle, and it's very important that you feel comfortable with them. These statements allow you to control the flow of your program based on the current state. With this, your programs become dynamic pieces of information, and you can respond to changes in new and interesting ways.

Try building some expressions, and using them in your code to take different actions based on the state of your other variables.

Exercises

1. Write a small program that gets a word from the user and outputs different results based on the first letter of the string. For example, if the user says "Hello", have a print statement that does something unique for words that start with "h". You don't need to do every letter, but pick a few, and for the rest, have an associated else-statement.

2. Modify the tax program you wrote at the end of the last chapter to output an error if the user enters a number less than zero.

3. Take a complicated expression and place round brackets in the appropriate places to show where the operator precedence boundaries lie. For example,

3 * 7 + 4 >= 5 ** 2 - 7

is equivalent to:

(((3 * 7) + 4) >= ((5 ** 2) - 7))

What about a larger expression:

12 ** 7 + 6 * 4 / 2 < 99 * 85 + 5 - 3 ** 4