It’s been awhile since I’ve contributed to this series, but I figured it was finally time to talk about making choices in Python programs. Today, we’ll finally move past our sequential programs into the wild world of branches. I think you’re going to like this one!
Table of Contents
Imperative Programming Revisited
As we’ve gone through this series, we’ve covered a ton of concepts. For example, we spent time going over the history of the discipline to gain some appreciation for computer hardware. Likewise, we talked about type systems and the way they govern program data.
Well, we’re finally at a point in this series where we can do something a little more interesting. Up to this point, we’ve largely been playing with expressions and functions. For example, we spent the last article exploring several built-in functions which we can use to compute values.
At the end of the last article, I mentioned how hesitant I was to start exploring imperative programming. As a result, I had tried to exhaust as many topics as I could before we got here, but I think I’ve run out. In other words, it’s finally time we take on imperative programming.
Because I’m so worried about how this concept is taught, I’m going to take things very slowly. In particular, we’re going to take this entire article to introduce basic branching using if statements. Then, I’m going to write a couple follow-up articles that put these concepts into practice. To keep things interesting, we’ll use fun examples like Rock Paper Scissors.
That said, get excited because we’re finally going to be able to use your new algorithmic thinking knowledge. That’s right! Branching is how we make choices in imperative programming.
An Overview of Branching
In programming, branching refers to the act of breaking the sequential ordering of instructions in a program. Up to this point, we haven’t seen any code that does this—at least not explicitly. In other words, all of our programs have been executed line-by-line.
As you can probably imagine, having a program that executes in order is pretty convenient. After all, in order to figure out how the program works, we only have to trace each statement in order.
Of course, writing a branchless program isn’t always practical, readable, or even fast. In fact, I think it’s quite natural to think in terms of branches. Think about it! When we wrote our pizza ordering algorithm awhile back, we had to think about issues that could arise.
As it turns out, there were a lot of issues we didn’t account for. For example, here was our final algorithm in Python-like pseudocode:
phone_number = lookup(pizza_place) dial(phone, phone_number) employee = wait(phone) place(employee, order, credit_card, address) hang_up(phone)
One of the things we did when designing this algorithm was pass off responsibility to functions. In other words, if we didn’t know exactly how to handle something, we used a placeholder.
Of course, there are some major issues even with using placeholder functions. In this program’s current state, we aren’t accounting for any issues that could arise between steps.
For example, what happens if we lookup the phone number of the pizza place, and they don’t have one? Perhaps the lookup function will return -1 to let us know. As it currently stands, we’re going to try to dial -1 into our phone, and I can’t imagine that’s going to go very well.
If we find out our favorite pizza place doesn’t have a phone number, we need to be able to break out of the current sequence of instructions. This is where a branch is incredibly useful.
Introducing the If Statement
Any time we want to make a choice in our program, we most likely turn to an if statement. The general structure of an if statement works like this:
IF condition is true, THEN do something.
The key takeaway here is that an if statement creates a branch if and only if some condition is met. For example, we might create an if statement for our pizza ordering algorithm that reads as follows:
IF phone_number is -1, THEN try a pizza place that we know we can call
If and only if we have a bad phone number do we try something else. Otherwise, we can continue to run the program as normal. In fact, we run the remainder of the program as normal regardless. All the if statement does is inject some code before we dial the phone number if and only if our first attempt to get a phone number fails.
Interestingly, Python makes it very easy to incorporate these if statements into our program:
if condition: # do something
In this case, there are two pieces of information I want you to keep an eye on.
First, the `if`python keyword is followed by a condition. Put simply, a condition is any expression that evaluates to a boolean: True of False. Here are a few examples:
3 < 4 # returns True True and False # returns False len("Hi") == 2 # returns True
That said, as we’ve discussed already, Python will accept pretty much anything as a condition. The trick is being able to identify what values are considered falsy (e.g. `0`, `””`, ``, etc.) . I recommend looking back at either the function-only or the type systems articles for more examples.
Note: a common mistake for new programmers is to compare a boolean to True in an if statement. For example, we might have a boolean called `is_tall`python which indicates if the user is tall when True. At some point, we decide to write some code to check if the user is tall, so we write the following condition: `is_tall == True`python. While this isn’t technically wrong, it’s a bit redundant, so we’re more likely to see `is_tall`python by itself in professional code.
Second, any lines of code inside the if statement must be indented. This is one of the interesting aspects of Python: it’s whitespace sensitive. In other words, if there is any code that we don’t want to be executed inside the if statement, we should not indent it:
x = 10 if x > 5: x += 3 # x becomes 13 x -= 4 # x becomes 9
In this example, the if statement condition is true, so we add 3 to our variable. Afterward, we subtract 4 from it to end up with 9. If, however, the condition was false, we would end up with a very different sequence of instructions:
x = 4 if x > 5: x += 3 # does not execute x -= 4 # x becomes 0
This time, instead of executing line 3, we jump straight to line 4 and subtract 4. As a result, x stores 0.
Introducing the Else Statement
Wherever there is an if statement, there is an opportunity for an else statement. The basic idea here is that if that if condition isn’t met, the else branch will be executed. This ensures that you always execute one of the two branches. Here’s what that looks like in Python code:
if condition: # do something else: # do something else
Again, it is important that for both of these branches, we respect the whitespace. In other words, we can have a multiline branch as long as we keep our code indented properly as follows:
if condition: # first line of if branch # second line of if branch # third line of if branch else: # do something else # no, really!
With these concepts in mind, let’s revisit our arithmetic examples with an added else branch:
x = 10 if x > 5: x += 3 # x becomes 13 else: x -= 4 # does not execute
Now that our variable is only modified in both branches, it will only be updated based on which condition returns true. In this case, 10 is greater than 5, so we add 3 to our variable. However, if we once again changed our initial value for our variable, we would see a different result:
x = 4 if x > 5: x += 3 # does not execute else: x -= 4 # x becomes 0
This type of design is useful when we know we only have two outcomes. If our condition doesn’t meet one of the outcomes, we know we need to run the other branch. As we’ll see later, the else statement is also useful for handling our default cause. In other words, when all else fails, run the else branch.
Introducing the Elif Statement
At this point, we’ve introduced the two main mechanisms for branching: if and else. Unfortunately, this only leaves us with two possibilities when in reality, there may be many different possibilities. That’s where the elif statement comes in.
If an if statement kicks off branching, and the else statement ends branching, then the elif statement must fit somewhere in the middle. Here’s what that looks like in code:
if condition: # do something elif other_condition: # do this other thing else: # do something else
Again, the elif statement is bound by all the same rules as the if statement and the else statement. In other words, all code meant to be inside the elif branch must be indented.
Likewise, elif statements are a lot like if statements in that they must be followed by a condition. If we’re reading the example above out loud, we might say:
“if condition is true, do something. If not, try this other condition. If it’s true, do this other thing. Otherwise, do something else.”
Interestingly, this elif statement is the only statement out of the three that can be repeated indefinitely. As a result, we can test as many conditions as we want:
if condition: # do something elif other_condition: # do this other thing elif third_condition: # do this third thing else: # do something else
Certainly, this can get out of hand, but this allows us to handle multiple possible mutually exclusive conditions. Here’s what that might look like for our arithmetic example:
x = 4 if x > 5: x += 3 # does not execute elif x % 2 == 0: x /= 2 # x becomes 2 else: x -= 4 # does not execute
Here, we’ve introduced a third condition that checks if our variable is even. Since 4 is even, we divide it by 2 and store the result. How’s that for a branching lesson?
In the next section, we’ll look at some of the consequences of including branches in our code.
Zen of Python: Blocks
Now that we’re adding quite a bit of complexity to our programs, it’s time to introduce a set of guiding principles for Python called the Zen of Python. It’s a short list, and it goes something like this:
Beautiful is better than ugly.PEP 20
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one– and preferably only one –obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea — let’s do more of those!
Don’t worry about memorizing this or even understanding most of it. I’ll continue to share the important lines as we go along. For instance, the line we care about today is line 5:
In this particular line, we make a comparison between two terms: flat and nested. As we can probably imagine, these terms refer to the structure of the code. In other words, up until this article, we’ve largely been talking about flat code: code which consists of a single sequence of statements.
Unfortunately, when we introduce branching, we introduce nesting. This can be seen through the indentation that we see on the line following the if statement:
if condition: # indented line of code
This indentation signals a new block which introduces a new context or scope. When we create these nested blocks, we inherit state from the outer block. For example, if we defined some variable at the top of our code, we could access that variable within the nested block. This is something we saw in our arithmetic example in the previous section.
On one hand, it’s great that we can access variables from outside our current block. On the other hand, this introduces a bit of complexity to our code. Now, we have to concern ourselves with the state of the program up to the point of the new block before we can evaluate anything. At a depth of 1, maybe this isn’t so bad. That said, the Zen of Python recommends avoiding nesting as much as possible, and you can see why:
x = 5 y = 3 if x > 2: y += 3 if x > 4: x *= 2 else: if y < 10: ...
That’s right! There are no limits to the where we can place if statements. As a result, we can place them within blocks which creates yet more blocks.
Unfortunately, it’s very easy to get lost in this code as our brains just aren’t designed to deal with remembering a lot of things at the same time (i.e., working memory capacity). As a result, a lot of Python folks will recommend limiting nesting if possible. As we continue in this series, we’ll take some time to show what this means in more detail. For now, keep this in the back of your mind.
Applying Branching to Actual Code
If you’re a fan of Spider-Man, you’ve probably heard the phrase: “with great power comes great responsibility.” I particularly like this quote because it’s a helpful principle to keep in mind as you learn to code—especially now that we’ve introduced branching.
As we continue to introduce complexity in our code, we continue to gain great power. However, we have to be careful about how we use that power. Unfortunately, programming is such a young field that there aren’t really any experts to coach you in how to contain that power. That said, there are a lot of opinions out there. For example, I gave you one tip that you might follow from the Zen of Python.
As you continue in your journey, I ask that you focus less on the problem solving aspect of programming—that will come, don’t worry—and more on how you use the knowledge you’ve learned for good. In other words, as you read my coding examples, consider how they could be written better for your understanding and implement that in your own coding.
To help you along with your development, we’ll be taking a detour over the next article of two to apply our new knowledge to actual problems. I look forward to seeing you then!
In the the meantime, I recommend checking out some of these Python articles that will show you branching in real code:
- How to Check if a List is Empty in Python: Type Flexibility and More
- How to Round a Number in Python: Truncation, Arithmetic, and More
- How to Compute Absolute Value in Python: Control Flow, Abs(), and More
Likewise, here are some Python resources from the folks at Amazon (#ad):
- Effective Python: 90 Specific Ways to Write Better Python
- Python Tricks: A Buffet of Awesome Python Features
- Python Programming: An Introduction to Computer Science
If you’re finding value in this series and would like to support the site, check out this list of ways to grow the site. I appreciate the support, and I hope you’ll stick around!
Recent Code Posts
Now that we're back to school, I figured it was time to talk about another introductory programming concept: unit testing. This time, I bring you some of my favorite tips targeted at folks who may be...
Operator overloading is an amazing feature that I've come to love as someone who came from Java, but it certainly can be abused. I decided to give that a try!