Flow Control

A computer program is like a journey for your data. Data encounters situations that impact it during this journey, changing it forever. Like any journey, one has a choice of routes to reach the destination. Sometimes, data takes one path; sometimes, another. Which roads the data takes depends on the program's end goal.

When writing programs, you want your data to take the correct path. You want it to turn left or right, up, down, reverse, or proceed straight ahead when it's supposed to. We call this flow control.

How do we make data do the right thing? We use conditionals.

Conditionals

A conditional is a fork, or multiple forks, in the road. When your data arrives at a conditional, Python evaluates the conditional and tells the data where to go. The simplest conditionals use a combination of if statements with comparison and logical operators (<, >, <=, >=, ==, !=, and, or, and not) to direct traffic. They use the keywords if, elif, and else.

That's enough talking; let's write some code. Create a file named conditional.py with the following content:

value = int(input('Enter a number: '))

if value == 3:
    print('value is 3')

Run conditional.py at least twice.

  • The first time, enter the value 3.
  • The second and subsequent times, input any other integer value.

This example shows the simplest of if statements: it has a single block (one or more Python statements or expressions) that executes when the condition (value == 3) is True. Otherwise, Python bypasses the block. Regardless, execution eventually resumes with the first statement or expression after the if statement.

Thus, if the input value is 3, this code prints value is 3. The code doesn't print anything if the user inputs any other integer.

We can expand the if statement to include some code that runs when value is not 3:

value = int(input('Enter a number: '))

if value == 3:
    print('value is 3')
# highlight
else:
    print('value is NOT 3')
# endhighlight

Here, Python prints value is 3 if the user enters 3; otherwise, it prints value is NOT 3.

Note that the else block isn't a proper statement; it's part of the if statement.

We can nest if statements inside an outer if. In this next example, we nest an if statement inside the else block of the outer if:

value = int(input('Enter a number: '))

if value == 3:
    print('value is 3')
else:
    # highlight
    if value == 4:
        print('value is 4')
    else:
        print('value is NOT 3 or 4')
    # endhighlight

This time, run conditional.py at least three times:

  • The first time, enter the value 3.
  • The second time, enter the value 4.
  • The third and subsequent times, input any other integer value.

The indentation levels show how the code is supposed to work. In this case, Python:

  • prints value is 3 if the user inputs 3.
  • prints value is 4 if the user inputs 4.
  • prints value is NOT 3 or 4 if the user enters any other integer.

The sequence of operations begins on line 3, where we compare the user input against 3. If yes, line 4 runs; otherwise, we drop down to the else block. In the else block, we compare the input against 4 on line 6. If yes, line 7 runs; otherwise, we drop down to the inner else block and run the code on line 9.

We recommend avoiding nested if statements when possible. They quickly become difficult to read with multiple levels of nesting or longish code blocks. However, don't get twisted up trying to avoid them entirely. Keep the nesting to a modest 2 or 3 levels deep and use functions to isolate some of the more complex code.

You can rewrite the previous example using an elif block:

if value == 3:
    print('value is 3')
elif value == 4:
    print('value is 4')
else:
    print('value is NOT 3 or 4')

The elif block runs if value == 3 is False and value == 4 is True. The code produces the same results as the nested if.

You can have as many elif blocks as you need, but they all need to be after the if block and, if the code has one, before the else block. The elif conditionals are evaluated in the order they appear in the code.

Finally, if statement blocks may contain as many lines as you need:

if value == 3:
    print('value is 3')
    print('value is an odd number')
    print('value is a prime number')
elif value == 4:
    print('value is 4')
    print('value is an even number')
    print('value is NOT a prime number')
elif value == 9:
    print('value is 9')
    print('value is an odd number')
    print('value is NOT a prime number')
else:
    print('value is something else')

Every once in a while, you may want to create a block in an if statement that does nothing. We usually do this for readability purposes. However, blocks can't be empty. Instead, you have to use a pass statement.

if value == 3:
    print('value is 3')
elif value == 4:
    print('value is 4')
elif value == 9:
    pass # We don't care about 9
else:
    print('value is something else')

Adding a comment to a pass is good practice so future programmers know why it is there.

All statements in a block must be indented from the statement that begins the block. The indentation in a block must be consistent. If the first line of the block is indented 4 spaces, all statements in the block must be indented 4 spaces.

Nested blocks should be indented more than the containing block. For instance, if the current block is indented by 4 spaces from the outer block (the conventional amount of indentation), then a nested block inside that block should be indented by 8 spaces. Another nested block would bring the indentation to 12 spaces.

Be careful with your indentation. If you accidentally outdent some code, that will end the block. For instance:

if value == 1:
    print('value is one')
print('the value is odd')

If you meant to print both messages when value is 1, that's what will happen. However, Python will display the second message even if value is not 1.

Comparisons

Let's look at the comparison operators in some more depth so you can build more complicated conditional statements. Remember that comparison operators return a Boolean value: True or False.

The expressions to the left and right of an operator are its operands. For instance, the equality comparison x == y uses the == operator with two operands, x and y.

  • ==

    The equality operator returns True when the operands have equal values, False otherwise. We discussed == briefly in the Basics Operations chapter. It should be familiar, even if it still looks strange.

    print(5 == 5)                 # True
    print(5 == 4)                 # False
    
    print('abc' == 'abc')         # True
    print('abc' == 'abcd')        # False
    
    print(5 == '5')               # False
    
    print([1, 2, 3] == [1, 2, 3]) # True
    print([1, 2, 3] == [3, 2, 1]) # False
    

    In most cases, operands must have the same type and value to be equal. Thus, 5 is not equal to '5'. There are some places where you can mix types, however. For instance, integers and floats that are mathematically equivalent are usually, but not always, considered equal:

    print(5 == float(5))                # True
    
    big_num = 12345678901234567
    print(float(big_num) == big_num)    # False
    

    Enormous floats lack precision at around 18 significant digits on most modern machines. That can lead to surprises if you happen to work with big numbers.

    Comparisons with strings are case-sensitive. Thus, 'abc' is not equal to 'aBc'. You can use the str.lower and str.upper methods to achieve a case-insensitive comparison:

    print('abc' == 'aBc')                 # False
    print('abc'.lower() == 'aBc'.lower()) # True
    print('abc'.upper() == 'aBc'.upper()) # True
    

    In some non-US alphabets, converting text to upper or lower case isn't straightforward. For instance, in German, 'straße' and strasse are considered equivalent. However, the following code prints False:

    'straße'.lower() == 'strasse'.lower()
    

    The str.casefold method makes allowances for this issue and does the right thing:

    'straße'.casefold() == 'strasse'.casefold()
    

    While casefold is only needed when working with non-US characters, it's best practice in Python to use casefold instead of lower or upper, especially when comparing strings.

  • !=

    The inequality operator, !=, is =='s inverse: It returns False when == would return True, and True when == would return False. It returns False when the operands have the same type and value, True otherwise. Other than the return value, the behaviors of == and != are identical.

    print(5 != 5)             # False
    print(5 != 4)             # True
    print('abc' != 'abc')     # False
    print('abc' != 'aBc')     # True
    print(5 != '5')           # True
    
  • < and <=

    The less than operator (<) returns True when the value of the left operand has a value that is less than the value on the right, False otherwise. The less than or equal to operator (<=) is similar, but it also returns True when the values are equal; < returns False when the operands are equal.

    print(4 < 5)              # True
    print(5 < 4)              # False
    print(5 < 5)              # False
    
    print(4 <= 5)             # True
    print(5 <= 4)             # False
    print(5 <= 5)             # True
    
    print('4' < '5')          # True
    print('5' < '4')          # False
    print('5' < '5')          # False
    
    print('42' < '402')       # False
    print('42' < '420')       # True
    print('420' < '42')       # False
    

    The examples with strings are especially tricky here! Make sure you understand them. Python compares strings character-by-character, moving from left to right. It looks for the first character that differs from its counterpart in the other string. Once it finds differing characters, it compares them to determine the relationship.

    If both strings are equal up to the shorter string's length, as in the last two examples, the shorter one is considered less than the longer one.

  • > and >=

    The greater than operator (>) returns True when the value of the left operand has a value that is greater than the value on the right, False otherwise. The greater than or equal to operator (>=) is similar, but it also returns True when the values are equal; > returns False when the operands are equal.

    print(4 > 5)              # False
    print(5 > 4)              # True
    print(5 > 5)              # False
    
    print(4 >= 5)             # False
    print(5 >= 4)             # True
    print(5 >= 5)             # True
    
    print('4' > '5')          # False
    print('5' > '4')          # True
    print('5' > '5')          # False
    
    print('42' > '402')       # True
    print('42' > '420')       # False
    print('420' > '42')       # True
    

    As with < and <=, you can compare strings with the > and >= operators; the rules are similar.

Logical Operators

You're beginning to get a decent grasp of basic conditional flow. Let's take a few minutes to see how we can combine conditions to create more complex scenarios. The not, and, and or logical operators provide the ability to combine conditions:

  • not

    The not operator returns True when its operand is False and returns False when the operand is True. That is, it negates its operand.

    print(not True)           # False
    print(not False)          # True
    print(not(4 == 4))        # False
    print(not(4 != 4))        # True
    

    In these examples, Python first evaluates the expression on the right, then applies not to the result, thus negating it. For instance, we know that 4 == 4 is True, so not(4 == 4) is False.

    You can omit the parentheses in the last two examples. However, we don't recommend it. Operator precedence issues may occur if you let Python decide which operator to evaluate first. Use parentheses if you have anything more complex than a single identifier or literal to the right of not.

    Unlike most operators, not takes a single operand; it appears to the operator's right. Operators that take only one operand are called unary operators. Operators that take two operands are binary operators, though you'll rarely hear that term.

  • and and or

    The and operator returns True when both operands are True. It returns False when either operand is False. The or operator returns True when either operand is True and False when both operands are False.

    The following truth table shows how True and False interact with the and and or operators. You should memorize this table:

    A B A and B A or B
    True True True True
    True False False True
    False True False True
    False False False False

    For completeness, let's see a few examples:

    print((4 == 4) and (7 == 7))        # True
    print((4 == 4) and (7 == 3))        # False
    print((4 == 9) and (7 == 7))        # False
    print((4 == 9) and (7 == 3))        # False
    
    print((4 == 4) or (7 == 7))         # True
    print((4 == 4) or (7 == 3))         # True
    print((4 == 9) or (7 == 7))         # True
    print((4 == 9) or (7 == 3))         # False
    

    As with not, parentheses aren't always needed. However, the same "precedence" issues may occur. Always use parentheses if the corresponding operand is not a literal or identifier.

Short Circuits

The and and or operators use a mechanism called short circuit evaluation to evaluate their operands. Consider these two expressions:

is_red(item) and is_portable(item)
is_green(item) or has_wheels(item)

The first expression returns True when item is red and portable. If either condition is False, then the overall result must be False. Thus, if the program determines that item is not red, it doesn't have to determine whether it is also portable. Python short-circuits the rest of the expression by terminating evaluation if it determines that item isn't red. It doesn't need to call is_portable() since it already knows the expression must be False.

Similarly, the second expression returns True when item is either green or has wheels. When either condition is True, the overall result must be True. Thus, if the program determines that item is green, it doesn't have to decide whether it has wheels. Again, Python short-circuits the entire expression once it knows that item is green; the expression must be True.

Truthiness

Many languages can evaluate objects and values as either truthy or falsy. Python is no slouch here; it can too. It can evaluate every object's truthiness. Note that these terms are not synonymous with True, False, and Boolean. In addition, truthy and falsy are not actual objects or values. Instead, they are terms that describe how specific objects behave in a Boolean context.

Truthiness arises in conditional expressions, such as if and while statements. Conditional expressions don't need to produce Boolean values. Instead, Python only needs to determine their truthiness. In an if statement, a conditional expression that evaluates as truthy causes the if block to execute. The else or elif block runs when the expression evaluates as falsy.

Since conditionals only care about truthiness, we can use any expression as the condition. The expression will always be evaluated as truthy or falsy.

So, which values are truthy? Which are falsy? The built-in falsy values are as follows:

  • False, None
  • all numeric 0 values (integers, floats, complex)
  • empty strings: ''
  • empty collections: [], (), {}, set(), frozenset(), and range(0)
  • Custom data types can also define additional falsy value(s).

Okay, now that we know what's falsy, what's truthy? Everything else.

Enough yammering. Let's see some examples:

value = 5                     # 5 is truthy
if value:
    print(f'{value} is truthy')
else:
    print(f'{value} is falsy')
value = 0                     # 0 is falsy
if value:
    print(f'{value} is truthy')
else:
    print(f'{value} is falsy')

The first example prints 5 is truthy while the second prints 0 is falsy. This works since Python evaluates 5 as truthy and 0 as falsy.

You may sometimes see articles that speak of true and false values; this even happens in the Python documentation. The authors should probably talk of truthy and falsy evaluations, instead, not true and false. At Launch School, we want you to use truthy and falsy when speaking of truthiness, True and False when talking of booleans, and true and false when discussing truths and falsehoods.

You may have noticed how we took care to say things like evaluates as truthy. For brevity, you can simply describe expressions as either truthy or falsy. The "evaluates as" terminology is unnecessary.

Truthiness and Short-Circuit Evaluation

You may recall that the and and or logical operators cause short-circuit evaluation. You may not realize that the logical operators don't always return True or False. Both operators care only about the truthiness of their operands. The final value returned by an expression using and and or is the value of the final sub-expression evaluated by Python:

print(3 and 'foo')   # last evaluated op: 'foo'
print('foo' and 3)   # last evaluated op: 3
print(0 and 'foo')   # last evaluated op: 0
print('foo' and 0)   # last evaluated op: 0
print(3 or 'foo')    # last evaluated op: 3
print('foo' or 3)    # last evaluated op: 'foo'
print(0 or 'foo')    # last evaluated op: 'foo'
print('foo' or 0)    # last evaluated op: 'foo'
print('' or 0)       # last evaluated op: 0
print(None or [])    # last evaluated op: []

Suppose you have a logical expression that returns a non-Boolean object instead of a Boolean:

foo = None
bar = 'qux'
is_ok = foo or bar

In this code, is_ok gets set to the truthy value, 'qux'. In most cases, you can use 'qux' as though it were a Boolean True value. However, using a string as a Boolean isn't always the best way to write your code. It may even look like a mistake to another programmer trying to track down a bug. In some strange cases, it may even be a mistake.

You can readily address this with an if/else statement:

if foo or bar:
    is_ok = True
else:
    is_ok = False

This snippet sets is_ok to either True or False based on the truthiness of foo or bar. However, it is wordy. Many Python programmers would write this more concisely as:

is_ok = bool(foo or bar)

Logical Operator Precedence

As discussed in an earlier chapter, Python has precedence rules for evaluating expressions that use multiple operators and sub-expressions. The following list shows the precedence of the comparison operators from highest (top) to lowest (bottom).

  • ==, !=, <=, <, >, >= - Comparison
  • not - Logical NOT
  • and - Logical AND
  • or - Logical OR

Thus, if you have an expression like not x == y, you can know that x == y is evaluated first, then not is applied to the overall result. That is, not x == y is equivalent to not(x == y).

Things get really confusing when combining and and or in an expression. Even though and has higher precedence than or, the fact that both are short-circuiting operators adds a whole lot of complexity. For example, can you determine what the following code prints?

print(1 or 2 and 3)
print(0 or 2 and 3)
print(1 or 0 and 3)
print(1 or 2 and 0)
print(0 or 0 and 3)
print(0 or 2 and 0)
print(1 or 0 and 0)
print(0 or 0 and 0)

print(1 and 2 or 3)
print(0 and 2 or 3)
print(1 and 0 or 3)
print(1 and 2 or 0)
print(0 and 0 or 3)
print(0 and 2 or 0)
print(1 and 0 or 0)
print(0 and 0 or 0)

Go ahead and guess what will be output, then try running the code to see the results. In all likelihood, you will guess incorrectly at least once.

We're not going to try to explain what's happening with this code. While you might encounter some code like this in the future, the mixed and/or code will likely only be a small part of your problems.

In short: do not write code like this! If you must mix and and or, use parentheses to control how the code gets written. For instance, compare these two lines of code:

print((a and b) or (c and d))
print(a and b or c and d)

The first line, while complex, is easier to understand than the second.

To repeat, avoid mixing and and or in a single expression unless you use parentheses to control the order of evaluation.

match/case Statement

The last conditional flow structure we want to discuss is the match/case statement (or, more concisely, the match statement). A match statement, in it's most basic form, is similar to an if statement but has a different interface. It compares a single value against multiple values, whereas if can test multiple expressions with any condition.

The Python match statement was introduced in Python 3.10. You won't be able to use it in earlier versions, such as the 3.9 version provided by Cloud9 with the Amazon Linux 2023 operating system. However, it's straightforward to rewrite the match statements we will use as if statements.

match statements use the reserved words match and case. It's often easier to show rather than tell, and that's certainly the case with the match statement. First, create a file named match.py with this content:

value = 5

match value:
    case 5:
        print('value is 5')
    case 6:
        print('value is 6')
    case _: # default case
        print('value is neither 5 nor 6')
# value is 5

This example is functionally identical to the following if/else statement:

value = 5

if value == 5:
    print('value is 5')
elif value == 6:
    print('value is 6')
else:
    print('value is neither 5 nor 6')
# value is 5

You can see how similar they are, but you can also see how they differ. The match statement evaluates the expression value, compares its value to the value in each case, and executes the block associated with the first matching case. In this example, the value of the expression is 5; thus, the program executes the statements associated with case 5. The statements in the case _ block run when the expression doesn't match any other case blocks. It acts like the final else in an if statement and must be the last case block in the match statement.

If you want to match multiple values in a case, you can do so by using the | character to separate item values:

value = 5

match value:
    case 1 | 2 | 3 | 4:
        print('value is < 5')
    case 5 | 6:
        print('value is 5 or 6')
    case _: # default case
        print('value is not 1, 2, 3, 4, 5, or 6')
# value is 5 or 6

There are plenty of uses for match statements. They also have "pattern matching" abilities (which are beyond the scope of this book). They're potent tools in Python. If you're uncomfortable with them, play with the ones we presented above and watch how they respond to your changes. Test their boundaries and learn their capabilities. Curiosity will serve you well in your journey towards mastering Python. There is much to discover!

Ternary Expressions

We wanted to briefly show you something we call ternary expressions. The official name is conditional expression, but that's confusing since something like a == b is also a conditional expression. It is also sometimes called a ternary operator. However, it's not an operator.

A ternary expression is a concise way to choose between two values based on some condition. They are often used as an expression on the right side of an assignment, as function arguments, and as function return values.

Ternary expressions have the following structure:

value1 if condition else value2

Python first evaluates the condition. If it's truthy, the expression returns value1. Otherwise, it returns value2. Note that only one of value1 and value2 will be evaluated.

Consider the following code:

if shape.sides() == 3:
    print("Triangle")
else:
    print("Square")

That's easy enough to understand, but it is a bit wordy. We can eliminate the wordiness at the sacrifice of a little clarity:

print("Triangle" if shape.sides() == 3 else "Square")

Ternaries work particularly well when you need a way to handle missing or invalid data in output:

print('N/A' if value == None else value)

Should you use ternary expressions in your code? We recommend using them only when it doesn't sacrifice too much clarity. Don't use them as a substitute for every 4-line if/else statement or as a way to save keystrokes: they work best as expressions.

Remember that many Python programmers dislike ternary expressions since they are hard to read. If you decide that you like ternary expressions, that's fine. However, use them judiciously. In particular, ternaries should almost always be extremely simple and fit entirely on one 79-column line of code.

Summary

This chapter covered conditionals, comparisons, logical operators, and truthiness to control the flow of code execution. These are fundamental tools you'll need to become a Python developer.

Exercises

  1. To what values do the following expressions evaluate?

    False or (True and False)
    True or (1 + 2)
    (1 + 2) or True
    True and (1 + 2)
    False and (1 + 2)
    (1 + 2) and True
    (32 * 4) >= 129
    False != (not True)
    True == 4
    False == (847 == '847')
    

    Solution

    False or (True and False)     # False
    True or (1 + 2)               # True
    (1 + 2) or True               # 3
    True and (1 + 2)              # 3
    False and (1 + 2)             # False
    (1 + 2) and True              # True
    (32 * 4) >= 129               # False
    False != (not True)           # False
    True == 4                     # False
    False == (847 == '847')       # True
    

    Video Walkthrough

    Please register to play this video

  2. Write a function, even_or_odd, that determines whether its argument is an even or odd number. If it's even, the function should print 'even'; otherwise, it should print 'odd'. Assume the argument is always an integer.

    A number is even if you can divide it by two with no remainder. For instance, 4 is even since 4 divided by 2 has no remainder. Conversely, 3 is odd since 3 divided by 2 has a remainder of 1.

    To determine the remainder, you can use the % modulo operator shown in The Basics chapter.

    Solution

    def even_or_odd(number):
        if number % 2 == 0:
            print('even')
        else:
            print('odd')
    
    def even_or_odd(number):
        print('even' if number % 2 == 0 else 'odd')
    

    The solutions use the modulo operator (%) to determine whether the number is even. If the result of number % 2 is 0, the number is even.

    The second solution shows the equivalent solution using a ternary expression. The author isn't sure whether this is more readable than the first solution.

    Video Walkthrough

    Please register to play this video

  3. Without running the following code, what does it print? Why?

    def bar_code_scanner(serial):
        match serial:
            case '123':
                print('Product1')
            case '113':
                print('Product2')
            case '142':
                print('Product3')
            case _:
                print('Product not found!')
    
    bar_code_scanner('113')
    bar_code_scanner(142)
    

    Solution

    The output is:

    Product2
    Product not found!
    

    The first call to bar_code_scanner prints Product2 since the first case that matches '113' is the one on line 5. The second call prints Product not found! since the numeric value 142 doesn't match any of the case statements with string arguments. Instead, it matches the _ "default" case.

    Video Walkthrough

    Please register to play this video

  4. Refactor this statement to use a regular if statement instead.

    return ('bar' if foo() else qux())
    

    Solution

    if foo():
        return 'bar'
    else:
        return qux()
    

    Ternaries are most useful when both result values are given by simple expressions; anything more complicated than calling a function or accessing a variable or literal value can lead to unreadable code. Our original code is an excellent example of using the ternary expression; the refactoring merely demonstrates how the ternary works.

    Video Walkthrough

    Please register to play this video

  5. What does this code output, and why?

    def is_list_empty(my_list):
        if my_list:
            print('Not Empty')
        else:
            print('Empty')
    
    is_list_empty([])
    

    Solution

    The output is Empty since an empty list like [] is falsy. As a result, my_list on line 2 is falsy, so the else block runs instead of the if block.

    Video Walkthrough

    Please register to play this video

  6. Write a function that takes a string as an argument and returns an all-caps version of the string when the string is longer than 10 characters. Otherwise, it should return the original string. Example: change 'hello world' to 'HELLO WORLD', but don't change 'goodbye'.

    Solution

    def caps_long(string):
        if len(string) > 10:
            return string.upper()
        else:
            return string
    
    print(caps_long("Sue Smith"))     # Sue Smith
    print(caps_long("Sue Roberts"))   # SUE ROBERTS
    print(caps_long("Joe Shea"))      # Joe Shea
    print(caps_long("Joe Stevens"))   # JOE STEVENS
    

    Video Walkthrough

    Please register to play this video

  7. Write a function that takes a single integer argument and prints a message that describes whether:

    • the value is between 0 and 50 (inclusive)
    • the value is between 51 and 100 (inclusive)
    • the value is greater than 100
    • the value is less than 0
    number_range(0)     # 0 is between 0 and 50
    number_range(25)    # 25 is between 0 and 50
    number_range(50)    # 50 is between 0 and 50
    number_range(75)    # 75 is between 51 and 100
    number_range(100)   # 100 is between 51 and 100
    number_range(101)   # 101 is greater than 100
    number_range(-1)    # -1 is less than 0
    

    Solution

    def number_range(number):
        if number < 0:
            print(f'{number} is less than 0')
        elif number <= 50:
            print(f'{number} is between 0 and 50')
        elif number <= 100:
            print(f'{number} is between 51 and 100')
        else:
            print(f'{number} is greater than 100')
    

    Video Walkthrough

    Please register to play this video