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
# Conditions look like
print(1 == 1)
print(1 != 1)
True
False
# Just a single if
i_feel = "happy"

if i_feel == "happy":
    print("I feel happy")

    print("Still indented")

print("This always runs")
I feel happy
Still indented
This always runs
# if-elif-else
i_feel = "confused"

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 don't feel happy or unhappy
# An example with if-elif but no else
i_feel = "confused"

if i_feel == "happy":
    print("Happy")
elif i_feel == "sad":
    print("Sad")

print("Moving on!")
Didn't match.
Moving on!

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']
# Advanced: looking at how for loops work behind-the-scenes

# This is what a for loop does behind the scenes:
some_iterable = ["a", "b", "c"]
iterator_for_the_loop = iter(some_iterable)

# This happens for all the elements, so really should be a while loop
placeholder = next(iterator_for_the_loop)
print(placeholder)

placeholder = next(iterator_for_the_loop)
print(placeholder)

placeholder = next(iterator_for_the_loop)
print(placeholder)

# next(iterator_for_the_loop)

# To handle the end of the loop
try: 
    placeholder = next(iterator_for_the_loop)
except StopIteration:
    pass

# Equivalent code as for loop
for placeholder in some_iterable:
    print(placeholder)
a
b
c
a
b
c

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(20,-21, 4):
    print(i)

my_range = range(10)

sum(my_range)
45

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).

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
# Useful with a conditional
for fruit in fruits:
    print(fruit)
    if fruit == "banana":
        print("Yuck!")
        break
apple
banana
Yuck!

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", "pear", "cherry"]

for fruit in fruits:
    print(fruit)
    if fruit == "banana":
        print("Yuck!")
        break
else:
    print("Didn't hit break - no yucky fruit")
apple
pear
cherry
Didn't hit break - no yucky fruit

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

print("Moving on")
  Cell In[38], line 4
    print("Moving on")
    ^
IndentationError: expected an indented block after 'if' statement on line 1

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

if "banana" in fruits:
    pass # do this later
elif "apple" in fruits:
    pass 
else:
    pass

print("Moving on")
Moving on

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 = 500

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")
Something's wrong with the internet

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)

print(x * y)
3 4
12

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 = (4, 4)

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