Chapter 9

Nested Conditionals

Subsections of Nested Conditionals

Linear Conditionals

YouTube Video

Resources

To explore the various ways we can use conditional statements in code, let’s start by examining the same problem using three different techniques. We’ll use the classic Rock Paper Scissors game. In this game, two players simultaneously make one of three hand gestures, and then determine a winner based on the following diagram:

Rock Paper Scissors Rock Paper Scissors 1

For example, if the first player displays a balled fist, representing rock and the second player displays a flat hand, representing paper, then the second player wins because “paper covers rock” according to the rules. If the players both display the same symbol, the game is considered a tie.

So, to make this work, we’re going to write a Python program that reads two inputs representing the symbols, and then we’ll use some conditional statements to determine which player wins. A basic skeleton of this program is shown below:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    # determine the winner


main()

Our goal, therefore, is to replace the comment # determine the winner with a set of conditional statements and print statements to accomplish this goal.

Linear Conditional Statements

First, let’s try to write this program using what we already know. We’ve already learned how to use conditional statements, and it is completely possible to write this program using just a set of linear conditional statements and nothing else. So, to do this, we need to determine each possible combination of inputs, and then write an if statement for each one. Since there are $3$ possible inputs for each player, we know there are $3 * 3 = 9$ possible combinations:

Player 1 Player 2 Output
rock rock tie
rock paper player 2 wins
rock scissors player 1 wins
paper rock player 1 wins
paper paper tie
paper scissors player 2 wins
scissors rock player 2 wins
scissors paper player 1 wins
scissors scissors tie

We also have to deal with a final option where one player or the other inputs an invalid value. So, in total, we’ll have $10$ conditional statements in our program!

Let’s first try to make a quick flowchart of this program. Since we need $10$ conditional statements, our flowchart will have $10$ of the diamond-shaped decision nodes. Unfortunately, that would make a very large flowchart, so we’ll only include the first three decision nodes in the flowchart shown here:

Linear Flowchart Linear Flowchart

As we can see, this flowchart is very tall, and just consists of one decision node after another. Thankfully, we should know how to implement this in code based on what we’ve previously learned. Below is a full implementation of this program in Python, using just linear conditional statements:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    if p1 == "rock" and p2 == "rock":
        print("tie")
    if p1 == "rock" and p2 == "paper":
        print("player 2 wins")
    if p1 == "rock" and p2 == "scissors":
        print("player 1 wins")
    if p1 == "paper" and p2 == "rock":
        print("player 1 wins")
    if p1 == "paper" and p2 == "paper":
        print("tie")
    if p1 == "paper" and p2 == "scissors":
        print("player 2 wins")
    if p1 == "scissors" and p2 == "rock":
        print("player 2 wins")
    if p1 == "scissors" and p2 == "paper":
        print("player 1 wins")
    if p1 == "scissors" and p2 == "scissors":
        print("tie")
    if not (p1 == "rock" or p1 == "paper" or p1 == "scissors") and not (p2 == "rock" or p2 == "paper" or p2 == "scissors"):
        print("error")


main()

This program seems a bit complex, but if we step through it one step at a time it should be easy to follow. For example, if the user inputs "scissors" for player 1 and "rock" for player 2, we just have to find the conditional statement that matches that input and print the correct output. Since we were careful about how we wrote the Boolean expressions for these conditional statements, we know that it is only possible for one of them to evaluate to true. So, we’d say that those Boolean expressions are mutually exclusive, since it is impossible for two of them to be true at the same time.

Things get a bit more difficult in the last conditional statement. Here, we want to make sure the user has not input an invalid value for either player. Unfortunately, the only way to do that using linear conditional statements is to explicitly check each possible value and make sure that the user did not input that value. This can seem pretty redundant, and it indeed is! As we’ll see later in this lab, there are better ways we can structure this program to avoid having to explicitly list all possible inputs when checking for an invalid one.

Before moving on, it is always a good idea to run this code yourself, either directly in Python or using Python Tutor, to make sure you understand how it works and what it does.

Alternative Version

There is an alternative way we can write this program using linear if statements. Instead of having an if statement for each possible combination of inputs, we can instead have a single if statement for each possible output, and construct more complex Boolean expressions that are used in each if statement. Here’s what that would look like in Python:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    if (p1 == "rock" and p2 == "rock") or (p1 == "paper" and p2 == "paper") or (p1 == "scissors" and p2 == "scissors"):
        print("tie")
    if (p1 == "rock" and p2 == "paper") or (p1 == "paper" and p2 == "scissors") or (p1 == "scissors" and p2 == "rock"):
        print("player 2 wins")
    if (p1 == "rock" and p2 == "scissors") or (p1 == "paper" and p2 == "rock") or (p1 == "scissors" and p2 == "paper"):
        print("player 1 wins")
    if not (p1 == "rock" or p1 == "paper" or p1 == "scissors") and not (p2 == "rock" or p2 == "paper" or p2 == "scissors"):
        print("error")


main()

In this example, we now have just four if statements, but each one now requires a Boolean expression that includes multiple parts. So, this program is simultaneously more and less complex than the previous example. It has fewer lines of code, but each line can be much trickier to understand and debug.

Again, feel free to run this code in either Python Tutor or directly in Python to confirm that it works and make sure you understand it before continuing.

In terms of style, both of these options are pretty much equivalent - there’s no reason to choose one over the other. However, we’ll see later in this lab, there are much better ways we can write this program using chaining and nesting with conditional statements.

note-1

The Python programming language has its own style guide, known as “PEP 8” to most Python programmers. One of the major conventions proposed in that guide is limiting the length of each line of code to just 79 characters, in order to make the code more readable. However, as we’ve seen above, it is very easy to exceed that length when dealing with complex Boolean expressions, and we’ll see this again as we add multiple levels of indentation when we nest conditional statements.

In this course, we won’t worry about line length, even though some text editors may mark those lines as being incorrect in Python. Likewise, in many examples we won’t wrap the lines to a shorter length, except for readability purposes in the videos and slides.

That said, it’s definitely a good style to try and follow, and we encourage you to think about ways you can write your code to keep it as concise and readable as possible. Having shorter lines of code, while still using descriptive variable and function names, is a good start!


  1. File:Rock-paper-scissors.svg. (2020, November 18). Wikimedia Commons, the free media repository. Retrieved 16:57, February 28, 2022 from https://commons.wikimedia.org/w/index.php?title=File:Rock-paper-scissors.svg&oldid=513212597.]  ↩︎

Subsections of Linear Conditionals

Chaining Conditionals

YouTube Video

Resources

In the previous example, we saw a set of linear if statements to represent a Rock Paper Scissors game. As we discussed on that page, the Boolean expressions are meant to be mutually exclusive, meaning that only one of the Boolean expressions will be true no matter what input the user provides.

When we have mutually exclusive Boolean expressions like this, we can instead use if-else statements to make the mutually exclusive structure of the program clearer to the user. Let’s see how we can do that.

Chaining Conditional Statements

To chain conditional statements, we can simply place the next conditional statement on the False branch of the first statement. This means that, if the first Boolean expression is True, we’ll execute the True branch, and then jump to the end of the entire statement. If it is False, then we’ll go to the False branch and try the next conditional statement. Here’s what this would look like in a flowchart:

Chained Flowchart Chained Flowchart

This flowchart is indeed very similar to the previous one, but with one major change. Now, if any one of the Boolean expressions evaluates to True, that branch will be executed and then the control flow will immediately drop all the way to the end of the program, without ever testing any of the other Boolean expressions. This means that, overall, this program will be a bit more efficient than the one with linear conditional statements, because on average it will only have to try half of them before the program ends.

Now let’s take a look at what this program would look like in Python:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    if p1 == "rock" and p2 == "rock":
        print("tie")
    else:
        if p1 == "rock" and p2 == "paper":
            print("player 2 wins")
        else: 
            if p1 == "rock" and p2 == "scissors":
                print("player 1 wins")
            else:
                if p1 == "paper" and p2 == "rock":
                    print("player 1 wins")
                else:
                    if p1 == "paper" and p2 == "paper":
                        print("tie")
                    else:
                        if p1 == "paper" and p2 == "scissors":
                            print("player 2 wins")
                        else:
                            if p1 == "scissors" and p2 == "rock":
                                print("player 2 wins")
                            else:
                                if p1 == "scissors" and p2 == "paper":
                                    print("player 1 wins")
                                else:
                                    if p1 == "scissors" and p2 == "scissors":
                                        print("tie")
                                    else:
                                        if not (p1 == "rock" or p1 == "paper" or p1 == "scissors") and not (p2 == "rock" or p2 == "paper" or p2 == "scissors"):
                                            print("error")


main()

As we can see, this program is basically the same code as the previous program, but with the addition of a number of else keywords to place each subsequent conditional statement into the False branch of the previous one. In addition, since Python requires us to add a level of indentation for each conditional statement, we see that this program very quickly becomes difficult to read. In fact, the last Boolean expression is so long that it doesn’t even fit well on the screen!

We can make a similar change to the other example on the previous page:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    if (p1 == "rock" and p2 == "rock") or (p1 == "paper" and p2 == "paper") or (p1 == "scissors" and p2 == "scissors"):
        print("tie")
    else:
        if (p1 == "rock" and p2 == "paper") or (p1 == "paper" and p2 == "scissors") or (p1 == "scissors" and p2 == "rock"):
            print("player 2 wins")
        else:
            if (p1 == "rock" and p2 == "scissors") or (p1 == "paper" and p2 == "rock") or (p1 == "scissors" and p2 == "paper"):
                print("player 1 wins")
            else:
                if not (p1 == "rock" or p1 == "paper" or p1 == "scissors") and not (p2 == "rock" or p2 == "paper" or p2 == "scissors"):
                    print("error")


main()

Again, this results in very long lines of code, but it still makes it easy to see that the program is built in the style of mutual exclusion, and only one of the True branches will be executed. As before, feel free to run these programs directly in Python or using Python Tutor to confirm they work and that you understand how they work before continuing.

The Final Case

Now that we’ve built a program structure that enforces mutual exclusion, we’ll might notice something really interesting - the final if statement is no longer required! This is because we’ve already exhausted all other possible situations, so the only possible case is the last one. In that case, we can remove that entire statement and just replace it with the code from the True branch. Here’s what that would look like in Python:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    if p1 == "rock" and p2 == "rock":
        print("tie")
    else:
        if p1 == "rock" and p2 == "paper":
            print("player 2 wins")
        else: 
            if p1 == "rock" and p2 == "scissors":
                print("player 1 wins")
            else:
                if p1 == "paper" and p2 == "rock":
                    print("player 1 wins")
                else:
                    if p1 == "paper" and p2 == "paper":
                        print("tie")
                    else:
                        if p1 == "paper" and p2 == "scissors":
                            print("player 2 wins")
                        else:
                            if p1 == "scissors" and p2 == "rock":
                                print("player 2 wins")
                            else:
                                if p1 == "scissors" and p2 == "paper":
                                    print("player 1 wins")
                                else:
                                    if p1 == "scissors" and p2 == "scissors":
                                        print("tie")
                                    else:
                                        # All other options have been checked
                                        print("error")


main()

In this code, there is a comment showing where the previous if statement was placed, and now it simply prints an error. Again, this is possible because we’ve tried every possible valid combination of inputs in the previous Boolean expressions, so all that is left is invalid input. Try it yourself! See if you can come up with any valid input that isn’t already handled by the Boolean expressions - there shouldn’t be any of them.

Elif Keyword

To help build programs that include chaining conditional statements, Python includes a special keyword elif for this exact situation. The elif keyword is a shortened version of else if, and it means to replace the situation where an if statement is directly placed inside of an else branch. So, when we are chaining conditional statements, we can now do so without adding additional levels of indentation.

Here’s what the previous example looks like when we use the elif keyword in Python:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    if p1 == "rock" and p2 == "rock":
        print("tie")
    elif p1 == "rock" and p2 == "paper":
        print("player 2 wins")
    elif p1 == "rock" and p2 == "scissors":
        print("player 1 wins")
    elif p1 == "paper" and p2 == "rock":
        print("player 1 wins")
    elif p1 == "paper" and p2 == "paper":
        print("tie")
    elif p1 == "paper" and p2 == "scissors":
        print("player 2 wins")
    elif p1 == "scissors" and p2 == "rock":
        print("player 2 wins")
    elif p1 == "scissors" and p2 == "paper":
        print("player 1 wins")
    elif p1 == "scissors" and p2 == "scissors":
        print("tie")
    else:
        # All other options have been checked
        print("error")


main()

There we go! That’s much easier to read, and in fact it is much closer to the examples of linear conditionals on the previous page. This is the exact same program as before, but now it is super clear that we are dealing with a mutually exclusive set of Boolean expressions. And, we can still have a single else branch at the very end that will be executed if none of the Boolean expressions evaluates to True.

A structure of mutually exclusive statements like this is very commonly used in programming, and Python makes it very simple to build using the elif keyword.

note-1

Using chained conditional statements like this makes it easy to detect and handle errors in the final else block, since all other options have already been checked. However, some programmers prefer to explicitly check for errors in the input at the start of the code, before any other work is done. This is especially common when working with loops to prompt the user for new input in case of an error, which we’ll learn about in a later lab.

When you are reading code, it is important to check both the start and end of a block of code when looking for possible error checks, since they could be included in either place. Recognizing common coding styles and conventions such as where to check for errors will help us better understand code written by others, and also make our code more readable by others.

Subsections of Chaining Conditionals

Nesting Conditionals

YouTube Video

Resources

We’ve already seen how we can chain conditional statements by placing a new conditional statement inside of the False branch of another conditional statement. If we think about that, however, that implies that we probably should be able to place conditional statements inside of the True branch as well, or really anywhere. As it turns out, that’s exactly correct. We call this nesting, and it is really quite similar to what we’ve already seen in this lab.

Using nested conditional statements, we can really start to rethink the entire structure of our program and greatly simplify the code. Let’s take a look at how we can do that with our Rock Paper Scissors game.

Nesting Conditional Statements

In our Rock Paper Scissors game, we are really checking the value of two different variables, p1 and p2. In all of our previous attempts, we built complex Boolean expressions that checked the values of both variables in the same expression, such as p1 == "rock" and p2 == "scissors". What if we simply checked the value of one variable at a time, and then used nested conditional statements in place of the and operator? What would that look like?

Here’s an example of a Rock Paper Scissors game that makes use of nested conditional statements:

Nested Conditionals Nested Conditionals

Here, we begin by checking if the value in p1 is "rock". If it is, then we’ll go to the True branch, and start checking the values of p2. If p2 is also "rock", then we know we have a tie and we can output that result. If not, we can check to see if it is "paper" or "scissors", and output the appropriate result. If none of those are found, then we have to output an error.

In the False branch of the first conditional statement, we know that p1 is not "rock", so we’ll have to check if it is "paper" and "scissors". Inside of each of those conditional statements, we’ll also have to check the value of p2, so we’ll end up with a significant number of conditional statements in total!

Let’s see what such a program would look like in Python:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    if p1 == "rock":
        if p2 == "rock":
            print("tie")
        elif p2 == "paper":
            print("player 2 wins")
        elif p2 == "scissors":
            print("player 1 wins")
        else:
            print("error") 
    elif p1 == "paper":
        if p2 == "rock":
            print("player 1 wins")
        elif p2 == "paper":
            print("tie")
        elif p2 == "scissors":
            print("player 2 wins")
        else:
            print("error")
    elif p1 == "scissors":
        if p2 == "rock":
            print("player 2 wins")
        elif p2 == "paper":
            print("player 1 wins")
        elif p2 == "scissors":
            print("tie")
        else:
            print("error")
    else:
        print("error") 


main()

In this example, we have an outer set of chained conditional statements checking the value of p1, and then each of the branches will check the value of p2 and determine which output is correct. It is very similar in structure to the chained conditional example on the previous page, just laid out a bit differently. As before, try running this program yourself in either Python or Python Tutor to make sure it works and you understand how it is structured.

Removing Duplicate States

Looking at this code, one thing we might quickly notice is that we now have four places that print "error" instead of just one. This is because we now have to check the values of p2 in three separate places, and can’t simply assume that the final else case in the outermost conditional statement is the only case where an error might be found.

One way we can simplify this code is by including a specific conditional statement just to check for any error situations, and handle those upfront before the rest of the code. So, we can rewrite this code as shown here to accomplish that:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    if not (p1 == "rock" or p1 == "paper" or p1 == "scissors") or not (p2 == "rock" or p2 == "paper" or p2 == "scissors"):
        print("error")
    else:
        if p1 == "rock":
            if p2 == "rock":
                print("tie")
            elif p2 == "paper":
                print("player 2 wins")
            else:
                print("player 1 wins")
        elif p1 == "paper":
            if p2 == "rock":
                print("player 1 wins")
            elif p2 == "paper":
                print("tie")
            else:
                print("player 2 wins")
        else:
            if p2 == "rock":
                print("player 2 wins")
            elif p2 == "paper":
                print("player 1 wins")
            else:
                print("tie")


main()

By confirming that both p1 and p2 only contain either "rock", "paper", or "scissors" first, we can then make some pretty handy assumptions later in our code. For example, now the outermost conditional statement only explicitly checks if p1 contains "rock" or "paper", and then the third block is simply an else clause by itself. We can do this because we already know that "scissors" is the only other possible value that can be stored in p1, so we don’t have to explicitly check for it. We can make similar changes to the nested conditional statements as well. So, just by adding one complex Boolean expression and conditional statement to our program, we were able to remove 4 that we no longer needed!

Further Simplification

In fact, we can even take this one step further. Now that we know that both p1 and p2 only contain valid values, we can easily determine if that match has ended in a tie by simply checking if the variables contain the same value. So, with a bit of restructuring, we can simplify our program as shown here:

def main():
    p1 = input("Enter 'rock', 'paper', or 'scissors' for player 1: ")
    p2 = input("Enter 'rock', 'paper', or 'scissors' for player 2: ")

    if not (p1 == "rock" or p1 == "paper" or p1 == "scissors") or not (p2 == "rock" or p2 == "paper" or p2 == "scissors"):
        print("error")
    else:
        if p1 == p2:
            print("tie")
        elif p1 == "rock":
            if p2 == "paper":
                print("player 2 wins")
            else:
                print("player 1 wins")
        elif p1 == "paper":
            if p2 == "rock":
                print("player 1 wins")
            else:
                print("player 2 wins")
        else:
            if p2 == "rock":
                print("player 2 wins")
            else:
                print("player 1 wins")


main()

This code presents a clear, easy to read version of a Rock Paper Scissors program. At the top, we receive input from the users, and then the first conditional statement is used to determine if the input is valid. If not, it will print an error.

If the input is valid, then we can make some assumption about the various possible inputs in our program’s logic, which greatly reduced the number of conditional statements. We are also checking for ties at the beginning, so in the False branch of that conditional statement we can also assume that the values in p1 and p2 are different, further reducing the number of items we have to check.

We’ll revisit this example later in this course to show how we can convert the first conditional statement into a loop, which will prompt the user for new input if an invalid input is provided. This will make our program even better and easier for anyone to use.

Subsections of Nesting Conditionals

Blocks and Scope

YouTube Video

Resources

Now that we’ve seen how we can chain and nest multiple conditional statements in our code, we need to address a very important concept: variable scope.

In programming, variable scope refers to the locations in code where variables can be accessed. Contrary to what we may think based on our experience, when we declare a variable in our code, it may not always be available everywhere. Instead, we need to learn the rules that determine where variables are available and why.

Many Languages: Block Scope

First, let’s talk about the most common type of variable scope, which is block scope. This type of variable scope is used in many programming languages, such as Java, C#, C/C++, and more. In block scope, variables are only available within the blocks where they are declared, including any other blocks nested within that block. Also, variables can only be accessed by code executed after the variable is initially declared or given a value.

So, what is a block? Effectively, each function and conditional statement introduces a new block in the program. In languages such as Java and C/C++, blocks are typically surrounded by curly braces {}. In Python, blocks are indicated by the level of indentation.

For example, consider the following Python code:

def main():
    # main block
    x = int(input("Enter a number: "))
    if x > 5:
        # block A
        y = int(input("Enter a number: "))
        if y > 10: 
            # block B
            z = 10
        else:
            # block C
            z = 5
    elif x < 0:
        # block D
        a = -5
    else:
        # block E
        b = 0
    print("?")


main()

This program contains six different blocks of code:

  • The main block, which is all of the code within the main() function
  • Block A, which is all of the code inside of the True branch of the outermost conditional statement.
  • Block B, the True branch of the inner conditional statement
  • Block C, the False branch of the inner conditional statement
  • Block D, the True branch of the first elif clause of the outer conditional statement
  • Block E, the False branch of the outermost conditional statement

Inside of each of those blocks, we see various variables that are declared. For example, the variable x is declared in the main() function block. So, with block scope, this means that the variable x is accessible anywhere inside of that block, including any blocks nested within it.

Below is a list of all of the variables in this program, annotated with the blocks where that variable is accessible in block scope:

  • x - blocks main, A, B, C, D, E
  • y - blocks A, B, C
  • z - blocks B and C more on this later
  • a - block D
  • b - block E

One thing that is unique about this example is the variable z, which is declared in both block B and block C. What does this mean when we are dealing with block scope? Basically, those variables named z are actually two different variables! Since they cannot be accessed outside of the block that they are declared in, they should really be considered as completely separate entities, even though they are given the same name. This is one of the trickiest concepts when dealing with scope!

So, if this program is using block scope, what variables can be printed at the very end of the program, where the question mark ? is found in a print() statement? In this case, only the variable x exists in the main() function’s scope, so it is the only variable that we can print at the end of the program.

If we want to print the other variables, we must declare them in the appropriate scope. So, we can update our code as shown here to do that:

def main():
    # main block
    # variable declarations in main block
    y = 0
    z = 0
    a = 0
    b = 0
    x = int(input("Enter a number: "))
    if x > 5:
        # block A
        y = int(input("Enter a number: "))
        if y > 10: 
            # block B
            z = 10
        else:
            # block C
            z = 5
    elif x < 0:
        # block D
        a = -5
    else:
        # block E
        b = 0
    print("?")


main()

With this change, we can now access all of the variables in the main() function’s scope, so any one of them could be printed at the end of the program. This also has the side effect of making the variable z in both blocks B and C the same variable, since it is now declared at a higher scope.

Python: Function Scope

Python, however, uses a different type of variable scope known as function scope. In function scope, variables that are declared anywhere in a function are accessible everywhere in that function as soon as they’ve been declared or given a value. So, once we see a variable in the code while we’re executing a function, we can access that variable anywhere within the function.

Let’s go back to the previous example, and look at that in terms of function scope

def main():
    # main block
    x = int(input("Enter a number: "))
    if x > 5:
        # block A
        y = int(input("Enter a number: "))
        if y > 10: 
            # block B
            z = 10
        else:
            # block C
            z = 5
    elif x < 0:
        # block D
        a = -5
    else:
        # block E
        b = 0
    print("?")


main()

Using function scope, any of the variables x, y, z, a or b could be placed in the print() statement at the end of the main function, and the program could work. However, there is one major caveat that we must keep in mind: we can only print the variable if it has been given a value in the program!

For example, if the user inputs a negative value for x, then the variables y, z, and b are never given a value! We can confirm this by running the program in Python Tutor. As we see here, at the end of the program, only the variables x and a are shown in the list of variables within the main() frame in Python Tutor on the right:

Python Tutor Python Tutor

Because of this, we have to be careful when we write our programs in Python using function scope. It is very easy to find ourselves in situations where our program will work most of the time, since it usually executes the correct blocks of code to populate the variables we need, but there may be situations where that isn’t guaranteed.

This is where testing techniques such as path coverage are so important. If we have a set of inputs that achieve path coverage, and we are able to show that the variables we need are available at the end of the program in each of those paths, then we know that we can use them.

Alternatively, we can build our programs in Python as if Python used block scope instead of function scope. By assuming that variables are only available in the blocks where they are given a value, we can always ensure that we won’t reach a situation where we are trying to access a variable that isn’t available. For novice programmers, this is often the simplest option.

Dealing with scope is a tricky part of learning to program. Thankfully, in Python, as well as many other languages, we can easily use tools such as Python Tutor and good testing techniques to make sure our programs are well written and won’t run into any errors related to variable scope.

Subsections of Blocks and Scope

Worked Example

YouTube Video

Resources

Let’s go through another worked example to see how we can translate a problem statement into a working program. We’ll also take a look at how we can test the program to verify that it is working as intended.

Problem Statement

Here’s a short and simple game that can be played by two players:

Three players each guess a positive integer greater than $0$, and then share them simultaneously. The winner is chosen following this formula:

  • If any two players have chosen the same number, the game is a tie.
  • If all players have chosen even numbers, or all players have chosen odd numbers, then the smallest number wins.
  • Otherwise, the largest number wins.

This game has some similarities to Rock Paper Scissors, but the logic is quite a bit different. So, let’s work through how we would build this program using conditional statements.

Initial Program

Our first step should be to build a simple program that handles user input. So, we’ll create a new Python program that contains a main() function, a call to the main() function, and three variables to store user input. We’ll also use the input() function to read input, and the int() function to convert each input to an integer. Below that, we’ll add some print statements for testing. At this point, our program should look similar to this:

def main():
    p1 = int(input("Enter a positive integer for player 1: "))
    p2 = int(input("Enter a positive integer for player 2: "))
    p3 = int(input("Enter a positive integer for player 3: "))
    
    # debugging statements
    print("player 1 chose {}".format(p1))
    print("player 2 chose {}".format(p2))
    print("player 3 chose {}".format(p3))


main()

With this code in place, we’ve already created a Python program that we can run and test. So, before moving on, let’s run this program at least once to verify that it works correctly. This will help us quickly detect and correct any initial errors in our program, and make it much easer to debug logic errors later on.

So, when we run this program, we should see output similar to this:

Program Output 1 Program Output 1

Checking for Valid Input

Now that we have confirmed our program is working, let’s start writing the logic in this program. Deciding which conditional statement to write first is a very important step in designing a program, but it is difficult to learn what works best without lots of practice. If we look at the problem statement above, we see that there are several clear conditions that we’ll have to check:

  • Are the numbers all even?
    • If so, which number is smallest?
  • Are the numbers all odd?
    • If so, which number is smallest?
  • Are the numbers not all even or odd?
    • If so, which number is largest?

However, there is one more condition that we should also keep in mind. This one isn’t clearly stated in the rules, but implied in the problem statement itself:

  • Are all numbers greater than $0$?

So, to write an effective program, we should first make sure that all of the inputs are greater than $0$. We can do this using a simple conditional statement. We’ll also remove our debugging statements, as they are no longer needed:

def main():
    p1 = int(input("Enter a positive integer for player 1: "))
    p2 = int(input("Enter a positive integer for player 2: "))
    p3 = int(input("Enter a positive integer for player 3: "))
    
    if p1 <= 0 or p2 <= 0 or p3 <= 0:
        print("Error")
    else:
        print("All numbers are greater than 0")


main()

In this conditional statement, we are checking to see if any one of the numbers is less than or equal to $0$. Of course, using some Boolean algebra and De Morgan’s law, we can see that this is equivalent to checking if all numbers are not greater than $0$. Either approach is valid.

Checking for Ties

Once we know we have valid input, the next step in determining a winner is to first determine if there are any ties. For this, we simply need to check if any two players input the same number. Since there are three players, we need to have three different Boolean expressions to accomplish this. In our program, we could add a conditional statement as shown here:

def main():
    p1 = int(input("Enter a positive integer for player 1: "))
    p2 = int(input("Enter a positive integer for player 2: "))
    p3 = int(input("Enter a positive integer for player 3: "))
    
    if p1 <= 0 or p2 <= 0 or p3 <= 0:
        print("Error")
    else:
        if p1 == p2 or p2 == p3 or p3 == p1:
            print("Tie")
        else:
            print("Not a Tie")


main()

This is a pretty simple Boolean expression that will check and see if any possible pair of inputs is equal. We use the or Boolean operator here since any one of those can be true in order for the whole game to be a tie.

All Odds or Evens

Next, let’s tackle whether the inputs are all odds or all evens. If we look at the rules above, this seems to be the next most logical thing we’ll need to know in order to determine the winner of the game. Recall that we can determine if a number is even or odd by using the modulo operator % and the number $2$. If that result is $0$, the number is even. If not, the number is odd.

In code, we can express that in a conditional statement as shown here:

def main():
    p1 = int(input("Enter a positive integer for player 1: "))
    p2 = int(input("Enter a positive integer for player 2: "))
    p3 = int(input("Enter a positive integer for player 3: "))
    
    if p1 <= 0 or p2 <= 0 or p3 <= 0:
        print("Error")
    else:
        if p1 == p2 or p2 == p3 or p3 == p1:
            print("Tie")
        elif p1 % 2 == 0 and p2 % 2 == 0 and p3 % 2 == 0:
            print("All numbers are even")
        elif p1 % 2 != 0 and p2 % 2 != 0 and p3 % 2 != 0:
            print("All numbers are odd")
        else:
            print("Numbers are both even and odd")


main()

Notice that we are using the and Boolean operator in these conditional statements, because we want to be sure that all numbers are either even or odd.

In this example, we’re also choosing to use chained conditional statements with the elif keyword instead of nesting the conditionals. This helps clearly show that each outcome is mutually exclusive from the other outcomes.

However, we chose to nest the program logic inside of the outermost conditional statement, which checks for valid input. This helps us clearly see the part of the program that is determining who wins the game, and the part of the program that is validating the input. Later on, we’ll see how we can rewrite that conditional statement into a loop to prompt the user for new input, so it makes sense for us to keep it separate for now.

Determining the Smallest Number

Once we know if the numbers are either all even or all odd, we know that the winning number is the smallest number of the three inputs. So, how can we determine which number is the smallest? We can use a couple of nested conditional statements!

Let’s handle the situation where all numbers are even first. We know that the smallest number must be smaller than both other numbers. So, we can use a couple of Boolean expressions to check if that is the case for each number:

def main():
    p1 = int(input("Enter a positive integer for player 1: "))
    p2 = int(input("Enter a positive integer for player 2: "))
    p3 = int(input("Enter a positive integer for player 3: "))
    
    if p1 <= 0 or p2 <= 0 or p3 <= 0:
        print("Error")
    else:
        if p1 == p2 or p2 == p3 or p3 == p1:
            print("Tie")
        elif p1 % 2 == 0 and p2 % 2 == 0 and p3 % 2 == 0:
            if p1 < p2 and p1 < p3:
                print("Player 1 wins")
            elif p2 < p1 and p2 < p3:
                print("Player 2 wins")
            else:
                print("Player 3 wins")
        elif p1 % 2 != 0 and p2 % 2 != 0 and p3 % 2 != 0:
            print("All numbers are odd")
        else:
            print("Numbers are both even and odd")


main()

Here, we start by checking if player 1’s number is smaller than both player 2’s and player 3’s. If so, then player 1 is the winner. If not, we do the same for player 2’s number. If neither player 1 nor player 2 has the smallest number, then we can assume that player 3 is the winner without even checking.

As it turns out, we would end up using the exact same code in the situation where all numbers are odd, so for now we can just copy and paste that set of conditional statements there as well:

def main():
    p1 = int(input("Enter a positive integer for player 1: "))
    p2 = int(input("Enter a positive integer for player 2: "))
    p3 = int(input("Enter a positive integer for player 3: "))
    
    if p1 <= 0 or p2 <= 0 or p3 <= 0:
        print("Error")
    else:
        if p1 == p2 or p2 == p3 or p3 == p1:
            print("Tie")
        elif p1 % 2 == 0 and p2 % 2 == 0 and p3 % 2 == 0:
            if p1 < p2 and p1 < p3:
                print("Player 1 wins")
            elif p2 < p1 and p2 < p3:
                print("Player 2 wins")
            else:
                print("Player 3 wins")
        elif p1 % 2 != 0 and p2 % 2 != 0 and p3 % 2 != 0:
            if p1 < p2 and p1 < p3:
                print("Player 1 wins")
            elif p2 < p1 and p2 < p3:
                print("Player 2 wins")
            else:
                print("Player 3 wins")
        else:
            print("Numbers are both even and odd")


main()

That covers the situations where all players have input either even or odd numbers. We can quickly test this program a couple of times by providing various inputs that match those cases. So, when we run this program, we should see output like this:

Program Output 2 Program Output 2

Making a Function

However, as soon as we do that, we should start thinking about ways to simplify this program. This is because we are using the same exact code in multiple places in our program, which goes against one of the key principles of writing good programs: Don’t Repeat Yourself! .

Anytime we see this happen, we should start thinking of ways to move that code into a new function. This is actually really easy to do in this case: we can simply write a function named smallest() that accepts three numbers as input, representing the three player’s guesses, and then it can print the winning player for us. So, let’s update our program to include a new function, and then call that function from within our conditional statement:

def smallest(p1, p2, p3):
    if p1 < p2 and p1 < p3:
        print("Player 1 wins")
    elif p2 < p1 and p2 < p3:
        print("Player 2 wins")
    else:
        print("Player 3 wins")


def main():
    p1 = int(input("Enter a positive integer for player 1: "))
    p2 = int(input("Enter a positive integer for player 2: "))
    p3 = int(input("Enter a positive integer for player 3: "))
    
    if p1 <= 0 or p2 <= 0 or p3 <= 0:
        print("Error")
    else:
        if p1 == p2 or p2 == p3 or p3 == p1:
            print("Tie")
        elif p1 % 2 == 0 and p2 % 2 == 0 and p3 % 2 == 0:
            smallest(p1, p2, p3)
        elif p1 % 2 != 0 and p2 % 2 != 0 and p3 % 2 != 0:
            smallest(p1, p2, p3)
        else:
            print("Numbers are both even and odd")


main()

By using the same variable names as the parameters for the smallest() function, we can easily just copy/paste the conditional statement from the main() function into the smallest() function without making any changes to it. We already know it works, so we shouldn’t change it if we don’t have to.

Once again, now is a great time to test this program and make sure that everything is working before moving on.

Determining the Largest Number

The last step is to determine the largest number in the case that the numbers are not all even or odd. Since we’ve already written a function named smallest() to handle the opposite, let’s quickly write another function named largest() to handle this case. We can then call that function in the False branch of our conditional statement handling the logic of the program:

def smallest(p1, p2, p3):
    if p1 < p2 and p1 < p3:
        print("Player 1 wins")
    elif p2 < p1 and p2 < p3:
        print("Player 2 wins")
    else:
        print("Player 3 wins")


def largest(p1, p2, p3):
    if p1 > p2 and p1 > p3:
        print("Player 1 wins")
    elif p2 > p1 and p2 > p3:
        print("Player 2 wins")
    else:
        print("Player 3 wins")


def main():
    p1 = int(input("Enter a positive integer for player 1: "))
    p2 = int(input("Enter a positive integer for player 2: "))
    p3 = int(input("Enter a positive integer for player 3: "))
    
    if p1 <= 0 or p2 <= 0 or p3 <= 0:
        print("Error")
    else:
        if p1 == p2 or p2 == p3 or p3 == p1:
            print("Tie")
        elif p1 % 2 == 0 and p2 % 2 == 0 and p3 % 2 == 0:
            smallest(p1, p2, p3)
        elif p1 % 2 != 0 and p2 % 2 != 0 and p3 % 2 != 0:
            smallest(p1, p2, p3)
        else:
            largest(p1, p2, p3)


main()

There we go! We’ve written a complete program that implements the problem statement given above. Did you think it would end up being this complex? Sometimes even a simple problem statement ends up requiring quite a bit of code to implement it fully.

Testing - Branch Coverage

The next step is to perform some testing of our program to make sure it is fully working. To do that, we need to come up with a set of inputs that would achieve branch coverage, and possibly even path coverage. However, this time our program is spread across three functions, so it makes it a bit more difficult to do. So, let’s focus on each individual function separately and see if we can find a set that work for each of them.

Main Function

First, we see the main function has 5 branches to cover, so we need to come up with 5 different sets of inputs in order to achieve branch coverage. We can keep the inputs very small, just to make our testing a bit simpler. Here are the 5 branches to cover, and an input that will reach each one:

  • Invalid input: -1, -1, -1
  • Tie: 1, 1, 1
  • All Even: 2, 4, 6
  • All Odd: 1, 3, 5
  • Mixed: 1, 2, 3

Notice how we are trying to come up with the simplest possible inputs for each branch? That will make it easier to combine these inputs with the ones used in other functions to find an overall set of inputs that will achieve branch coverage for the entire program.

Smallest Function

Next, we can look at the function to find the smallest number. This function is called when the inputs are either all even or all odd. So, we know that either inputs 2, 4, 6 or 1, 3, 5 will call this function.

Within the function itself, we see three branches, depending on which player wins. So, if we start with the input 2, 4, 6, we’ll see that this will execute the branch where player 1 wins. To execute the other branches, we can simply reorder the inputs:

  • Player 1 Wins: 2, 4, 6
  • Player 2 Wins: 4, 2, 6
  • Player 3 Wins: 4, 6, 2

That will achieve branch coverage for the smallest() function, and even overlaps with one of the inputs used in the main() function.

Largest Function

The same applies to the function to find the largest number, but in this case we need a mix of even and odd numbers in the input. So, the input 1, 2, 3 will execute this function, and that input results in player 3 winning. Once again, we can reorder that input a bit to execute all three branches of this function:

  • Player 1 Wins: 3, 1, 2
  • Player 2 Wins: 1, 3, 2
  • Player 3 Wins: 1, 2, 3

Overall Program

Therefore, based on our analysis, we can achieve branch coverage across the entire program using 9 different inputs:

  • Invalid input: -1, -1, -1
  • Tie: 1, 1, 1
  • All Even:
    • Player 1 Wins: 2, 4, 6
    • Player 2 Wins: 4, 2, 6
    • Player 3 Wins: 4, 6, 2
  • All Odd: 1, 3, 5
  • Mixed:
    • Player 1 Wins: 3, 1, 2
    • Player 2 Wins: 1, 3, 2
    • Player 3 Wins: 1, 2, 3

This will execute each branch of all conditional statements in the program at least once.

Path Coverage

Once we’ve achieved branch coverage, we should also quickly look at path coverage: is there any possible pathway through this program that we haven’t tested yet? As it turns out, there are two, but they are somewhat difficult to find. Can you spot them? See if you can figure it out before continuing on in this page.

The paths we missed are situations where the numbers are all odd, but a player other than player 1 wins. While we tested all possible branches in the smallest() function, we didn’t test it using all odd numbers more than once. So, to truly achieve path coverage, we should add two more inputs to our list above:

  • Invalid input: -1, -1, -1
  • Tie: 1, 1, 1
  • All Even:
    • Player 1 Wins: 2, 4, 6
    • Player 2 Wins: 4, 2, 6
    • Player 3 Wins: 4, 6, 2
  • All Odd:
    • Player 1 Wins: 1, 3, 5
    • Player 2 Wins: 3, 1, 5
    • Player 3 Wins: 3, 5, 1
  • Mixed:
    • Player 1 Wins: 3, 1, 2
    • Player 2 Wins: 1, 3, 2
    • Player 3 Wins: 1, 2, 3

So, with a total of 11 inputs, we can finally say we’ve achieved both branch coverage and path coverage of this program.

Subsections of Worked Example

Summary

In this lab, we introduced several major important topics in Python. Let’s quickly review them.

Mutually Exclusive

Conditional statements are mutually exclusive when only one of the many branches will be executed for any possible input.

Chained Conditionals

if condition_1:
    print("1")
else
    if condition_2:
        print("2")
    else:
        if condition_3:
            print("3")
        else:
            print("4")

is equivalent to:

if condition_1:
    print("1")
elif condition_2:
    print("2")
elif condition_3:
    print("3")
else:
    print("4")

Nested Conditionals

if condition_1:
    if condition_2:
        print("1 and 2")
    elif condition_3:
        print("1 and 3")
    else:
        print("1 and not 2 or 3")
elif condition_4:
    if condition_2:
        print("4 and 2")
    elif condition_3:
        print("4 and 3")
    else:
        print("4 and not 2 or 3")
else:
    print("neither 1 nor 4")

Variable Scope

Variable scope refers to what parts of the code a particular variable is accessible in. Python uses function scope, which means that a variable defined anywhere in a function is available below that definition in any part of the same function.

Other languages use block scope, where variables are only available within the block where they are defined.

Subsections of Summary