More Control Flow Tools (Part I)

Today, we’re discussing sections 4.1-4.7 of The Python Tutorial. These are

Next month, we’ll cover 4.8-4.9: Functions

Intro

The goal of this section is to control how we run certain sections of code.

In Python, you create a section of code by indenting it:

# Outside any section

if True:
    ...
    # Inside a section
    # See? It's indented!

# Outside any section

4.1 if Statements

Conditionals: for running (indented) code only under certain conditions

  • if: always necessary
  • elif: optional, additional conditions (only if the previous fail)
  • else: optional, catches anything that fails
# Just a single if
i_feel = "happy"

if i_feel == "happy":
    print("I feel happy")
I feel happy
# if-elif-else
i_feel = "happy"

if i_feel == "happy":
    print("I feel happy")
elif i_feel == "unhappy":
    print("I feel unhappy")
else:
    print("I don't feel happy or unhappy")
I feel happy

4.2 for Statements

for statements let you run an indented block multiple times, once for each element in an iterable (something with multiple elements, like a list).

# Measure some strings:
words = ['cat', 'window', 'defenestrate']
for w in words:
    print(w, len(w))
cat 3
window 6
defenestrate 12

It’s possible to modify the iterable during the list. This can make things unpredictable, don’t do it. If you have to, use a copy of the object.

# Measure some strings:
words = ['cat', 'window', 'defenestrate']
for w in words.copy():
    print(w, len(w))
    words.append(w)

print(words)
cat 3
window 6
defenestrate 12
['cat', 'window', 'defenestrate', 'cat', 'window', 'defenestrate']

4.3 The range() Function

It’s common to loop over a range of numbers. The range() function makes that simpler. It represents a list of integers from start to stop every step:

# default start=0 and step=1
range(stop) 

# default step=1
range(start, stop) 

# with everything specified
range(start, stop, step)

for i in range(-10, 10, 2):
    print(i)
-10
-8
-6
-4
-2
0
2
4
6
8

range() objects have some weird properties

  • start is included but stop is excluded
  • You can use functions like sum() to add up the range
  • They don’t store all the numbers at once (makes them very efficient but you can’t index/subset them like lists. More on this next month)

4.4 break and continue Statements

Helper statements for loops.

break exits the loop:

fruits = ["apple", "banana", "cherry"]

for fruit in fruits:
    print(fruit)
    break
apple

continue skips to the start of the next iteration. These are both useful in conjunction with conditionals:

for fruit in fruits:
    if fruit == "banana":
        print("yuck!")
        continue
    
    print(fruit)
apple
yuck!
cherry

4.5 else Clauses on Loops

This is an odd but useful one. 99% of the time else statements are in if blocks and catch anything that fails the conditions.

When else follows a loop, it catches anything that didn’t break out of the loop. If the loop ends without break, the else condition will run.

Let’s see where this is useful:

fruits = ["apple", "banana", "cherry"]

for fruit in fruits:
    if fruit == "banana":
        print("yuck! I hate bananas.")
        break

# else connected to for loop, not if statement (compare indentation)
else:
    print("Phew! no bananas")
yuck! I hate bananas.

They also work for while loops. Remember them? They’re like conditionals+loops combined, they run an indented block until a condition is false.

4.6 pass Statements

These are simpler - pass statements do nothing. Nothing at all!

pass
if "banana" in fruits:
    pass
for fruit in fruits:
    pass

Why have them then? Because empty indented blocks throw an error:

if "banana" in fruits:
    # do this later
  Cell In[56], line 2
    # do this later
                   ^
SyntaxError: incomplete input

With pass, we can set up the framework but leave the implementation for later.

if "banana" in fruits:
    pass # do this later

4.7 match Statements

These are for pattern matching, and are the most advanced feature we’ll look at today. You don’t need to use these - an if stack can always do the job. But it’s useful and can be quick.

You match something to a case (usually multiple):

match something:
    case possibility_1:
        # some code
    case possibility_2:
        # some code

A ‘possibility’ of _ will match anything (it’s a wildcard). Cases are checked in order and only one is matched.

status = 418

match status:
    case 400:
        print("Bad request")
    case 404:
        print("Not found")
    case 418:
        print("I'm a teapot")
    case _:
        print("Something's wrong with the internet")
I'm a teapot

Combine several options with vertical bar | (“or”)

status = 401

match status:
    case 400:
        print("Bad request")
    case 401 | 403 | 404:
        print("Not allowed")
    case 404:
        print("Not found")
    case 418:
        print("I'm a teapot")
    case _:
        print("Something's wrong with the internet")
Not allowed

So far so good. It gets weird though. You can implicitly assign to variables if you put them in the patterns!

point = (3, 4)

match point:
    case (x, y):
        print(x, y)
3 4

This magically created the variables x and y and assigned their values!

A more complex example is given in The Python Tutorial

point = (3, 4)

match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        print("Not a point!")
X=3, Y=4

Lastly: guards. Let’s say the pattern matches but you still want to exclude specific cases. Use if to do this:

point = (3, 4)

match point:
    case (x,y) if x == y:
        print(f"Y=X at {x}")
    case (x, y):
        print(f"Not on the diagonal")
Not on the diagonal