Lecture 4: Control Flow

BAA1118 - Foundations of Programming for Analytics & AI

Damien Dupré

For Today

Requirements

  • ✅ Lists, dictionaries, indexing and slicing
  • ✅ Comparisons returning True or False

Programme

  1. Conditions: if, elif, else
  2. for loops
  3. while loops
  4. break, continue and comprehensions

Any questions?

Straight Line Code

Everything so far has run top to bottom, once.

units = 340
price = 12.99
print(units * price)
4416.6

Real analysis needs two more things:

  • Doing something only sometimes: conditions
  • Doing something many times: loops

Together they are called control flow: you control the order in which lines run.

1. Conditions

if

spend = 1250

if spend > 1000:
    print("Premium customer")
Premium customer
  • The line starts with if and ends with a colon
  • The block underneath is indented
  • The block runs only when the condition is True

Indentation Is Syntax

In most languages, indentation is decoration. In Python, it is the code.

if spend > 1000:
print("Premium")
  Cell In[45], line 2
    print("Premium")
    ^
IndentationError: expected an indented block after 'if' statement on line 1

The convention is 4 spaces. Colab and VS Code insert them for you when you press Enter after a colon.

Warning

Never mix tabs and spaces. It produces errors that are invisible on screen.

if / else

spend = 450

if spend > 1000:
    print("Premium customer")
else:
    print("Standard customer")
Standard customer

else has no condition. It catches everything the if did not.

if / elif / else

spend = 450

if spend > 1000:
    segment = "Premium"
elif spend > 500:
    segment = "Gold"
elif spend > 100:
    segment = "Silver"
else:
    segment = "Bronze"

print(segment)
Silver
  • Conditions are tested in order, top to bottom
  • The first true one runs, the rest are skipped
  • Order therefore matters enormously

if / elif / else

Source: UBC Master of Data Science, Programming in Python for Data Science

Order Matters

spend = 1250

if spend > 100:
    segment = "Silver"
elif spend > 1000:
    segment = "Premium"

print(segment)
Silver

The customer spent €1,250 and was labelled Silver, because > 100 was tested first and won.

Write cut-offs from the most restrictive to the least.

Combining Conditions

age = 34
spend = 1250

if age < 40 and spend > 1000:
    print("Young high spender")

if age > 65 or spend > 2000:
    print("Priority contact")

if not spend > 1000:
    print("Not a big spender")
Young high spender

Truthiness

You can put any value after if, not only a comparison. Python decides whether it counts as True or False:

  • Falsy values count as False: zero, empty or nothing (0, "", [], {}, None)
  • Truthy values count as True: everything else, such as 42 or "Dublin"
email = ""

if email:    # reads as "if there is an email"
    print("Send the newsletter")
else:
    print("No email on file")
No email on file

"" and None are both falsy, so if email: catches both.

Nested Conditions

country = "IE"
spend = 1250

if country == "IE":
    if spend > 1000:
        print("Irish premium")
    else:
        print("Irish standard")
else:
    print("International")
Irish premium

Each level adds 4 spaces. Beyond two levels, the code is hard to read: combine with and instead.

Your Turn: Grading

Write code that turns a mark out of 100 into a grade:

Mark Grade
70 and above First
40 to 69 Pass
below 40 Fail
  1. Store a mark in a variable, produce the grade with if / elif / else
  2. Print it with an f-string:
    A mark of 64 is a Pass
  3. Test it with 70, 40 and 39

Solution

Mark Grade Branch
70 and above First if
40 to 69 Pass elif
below 40 Fail else
mark = 64

if mark >= 70:       # 70 and above
    grade = "First"
elif mark >= 40:     # 40 to 69
    grade = "Pass"
else:                # below 40
    grade = "Fail"

print(f"A mark of {mark} is a {grade}")
A mark of 64 is a Pass
  • Each row of the table becomes one branch, in the same order
  • The elif needs no mark < 70: the if has already caught 70 and above
  • >= matters: with >, a mark of exactly 70 would be a Pass

2. for Loops

Repeating Over a Collection

Two ways to total a list without sum():

By hand: one line per element

sales = [120, 135, 148, 160]

total = 0
total = total + sales[0]
total = total + sales[1]
total = total + sales[2]
total = total + sales[3]
print(total)
563

With a for loop: one line, repeated

total = 0
for value in sales:
    total = total + value
    print(f"added {value}, total {total}")
added 120, total 120
added 135, total 255
added 148, total 403
added 160, total 563

Read the loop as English: “for each value in sales, add it to total”. The print() inside the loop shows each step.

Anatomy of a for Loop

for item in collection:
    do_something_with(item)
  • item is a new variable you name, holding one element at a time
  • collection is a list, string, dictionary, or anything iterable
  • The colon and the indented block, exactly as with if
  • The block runs once per element

Watching It Run

Source: UBC Master of Data Science, Programming in Python for Data Science

Watching It Run

for country in ["Ireland", "France", "Spain"]:
    print("Now processing:", country)
Now processing: Ireland
Now processing: France
Now processing: Spain

The loop variable survives after the loop, holding the last value:

print(country)
Spain

range()

For a fixed number of repetitions, range() generates the numbers.

for i in range(5):
    print(i)
0
1
2
3
4

range(5) gives 0, 1, 2, 3, 4: five numbers, starting at zero, stopping before 5. The same rule as slicing.

print(list(range(2, 8)))
print(list(range(0, 10, 2)))
[2, 3, 4, 5, 6, 7]
[0, 2, 4, 6, 8]

The Accumulator Pattern

Three steps you will write for the rest of your life:

sales = [120, 135, 148, 160]

# 1. start at zero
total = 0
count = 0

# 2. loop, updating
for value in sales:
    total += value
    count += 1

# 3. use the results
print("Total:", total)
print("Count:", count)
print("Mean:", total / count)
Total: 563
Count: 4
Mean: 140.75

What += means

total += value
# is short for
total = total + value

Add value to total, then store the result back in total.

count += 1 adds 1 each time round, so count ends up as the number of sales.

-=, *= and /= work the same way.

Building a New List

prices = [10.0, 25.0, 8.5]
prices_with_vat = []

for price in prices:
    prices_with_vat.append(round(price * 1.23, 2))

print(prices_with_vat)
[12.3, 30.75, 10.46]

Start with an empty list, append() inside the loop. This is the second most common loop pattern.

Why Not Start at 0?

A total starts at 0, so why does this list start at []? Try it with 0:

prices_with_vat = 0
prices_with_vat.append(12.3)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[61], line 2
      1 prices_with_vat = 0
----> 2 prices_with_vat.append(12.3)

AttributeError: 'int' object has no attribute 'append'
  • append() only exists for lists
  • 0 is an int, and an int has no append(): that is what the error says
  • Start with the empty version of what you are building: 0 for a total, [] for a list

Filtering Inside a Loop

Conditions and loops combine naturally:

spends = [45, 1200, 380, 2500, 90]
big_spenders = []

for spend in spends:
    if spend > 1000:
        big_spenders.append(spend)

print(big_spenders)
print(len(big_spenders), "customers above 1000")
[1200, 2500]
2 customers above 1000

Looping Over a Dictionary

customer = {"name": "Damien", "city": "Dublin", "spend": 1250.50}

for key in customer:
    print(key)
name
city
spend
for key, value in customer.items():
    print(f"{key}: {value}")
name: Damien
city: Dublin
spend: 1250.5

.items() gives both at once, and the two loop variables are unpacked exactly as with tuples.

Looping Over Nested Data

sales = [
    {"month": "Jan", "region": "North", "units": 120},
    {"month": "Jan", "region": "South", "units": 95},
    {"month": "Feb", "region": "North", "units": 138}
]

total_north = 0
for record in sales:
    if record["region"] == "North":
        total_north += record["units"]

print(total_north)
258

Eight lines. In week 8, pandas does this in one: df.groupby("region")["units"].sum().

enumerate() and zip()

When you need the position as well as the value:

countries = ["Ireland", "France", "Spain"]

for i, country in enumerate(countries):
    print(i, country)
0 Ireland
1 France
2 Spain

When you need to walk two lists together:

units = [120, 95, 138]

for country, unit in zip(countries, units):
    print(f"{country}: {unit}")
Ireland: 120
France: 95
Spain: 138

Your Turn: Loop Practice

monthly = [130, 120, 165, 185]
months = ["Jan", "Feb", "Mar", "Apr"]

Each task reuses one pattern from the previous slides:

  1. The total and the mean of monthly, without sum()
  2. A new list target: each value plus 10, using append()
  3. Print only the values above 150
  4. Use zip() to print each month with its value: Jan: 130

Solution

monthly = [130, 120, 165, 185]
months = ["Jan", "Feb", "Mar", "Apr"]

total = 0
count = 0
for value in monthly:
    total += value
    count += 1
print("Total:", total)
print("Mean:", total / count)
Total: 600
Mean: 150.0
target = []
for value in monthly:
    target.append(value + 10)
print(target)
[140, 130, 175, 195]
for value in monthly:
    if value > 150:
        print(value)
165
185
for month, value in zip(months, monthly):
    print(f"{month}: {value}")
Jan: 130
Feb: 120
Mar: 165
Apr: 185

3. while Loops

Repeating Until

A for loop runs a known number of times. A while loop runs until a condition stops being true.

balance = 1000
years = 0

while balance < 2000:
    balance = balance * 1.05
    years += 1

print(years, round(balance, 2))
15 2078.93

We did not know in advance that it would take 15 years. That is exactly when while is the right tool.

The Infinite Loop

count = 0
while count < 10:
    print(count)     # count never changes: this never ends

Important

Every while loop must contain a line that eventually makes the condition false. If Colab hangs, press the stop button, or Runtime -> Interrupt execution.

In practice, 95% of the loops you write in analytics are for loops.

break and continue

for value in [45, 1200, 380, 2500, 90]:
    if value > 1000:
        print("Found a big spender:", value)
        break            # stop the loop entirely
Found a big spender: 1200
for value in [45, None, 380, None, 90]:
    if value is None:
        continue         # skip to the next iteration
    print(value * 2)
90
760
180

continue is how you skip missing values without crashing.

Your Turn: Until and Skip

Until: while

  1. €5,000 grows by 3% a year. Adapt the €1,000 example: how many years until it passes €10,000?
  2. Rerun at 6%. Does the answer halve?

Skip and stop: continue, break

readings = [12.5, None, 15.0, None, 9.8]
  1. Print each reading, using continue to skip the None values
  2. Replace continue with break and rerun. What changes?

Solution

balance = 5000
years = 0
while balance < 10000:
    balance = balance * 1.03
    years += 1
print(years, "years at 3%")
24 years at 3%

24 years at 3%, 12 at 6%: the “rule of 72” (72 ÷ the rate) predicts both.

continue skips each None and carries on. break stops the loop at the first None, so only 12.5 is printed.

readings = [12.5, None, 15.0, None, 9.8]

for reading in readings:
    if reading is None:
        continue
    print(reading)
12.5
15.0
9.8
for reading in readings:
    if reading is None:
        break
    print(reading)
12.5

4. Comprehensions

The Loop, Compressed

Building a list with a loop:

prices = [10.0, 25.0, 8.5]

with_vat = []
for price in prices:
    with_vat.append(price * 1.23)
print(with_vat)
[12.3, 30.75, 10.455]

The same thing, in one line:

with_vat = [price * 1.23 for price in prices]
print(with_vat)
[12.3, 30.75, 10.455]

Reading a Comprehension

[expression for item in collection]
#     ^          ^        ^
#   what to    the loop variable
#   store      and where it comes from

Read it backwards: “for each price in prices, store price times 1.23”.

print([c.upper() for c in ["ireland", "france"]])
print([n ** 2 for n in range(6)])
['IRELAND', 'FRANCE']
[0, 1, 4, 9, 16, 25]

Comprehension With a Condition

spends = [45, 1200, 380, 2500, 90]

big = [s for s in spends if s > 1000]
print(big)
[1200, 2500]

And a condition on the value stored:

labels = ["big" if s > 1000 else "small" for s in spends]
print(labels)
['small', 'big', 'small', 'big', 'small']

Tip

If a comprehension no longer fits comfortably on one line, write it as a normal loop. Clever is not a goal.

Why Bother?

  • Shorter, and once you are used to it, clearer
  • It is everywhere in real Python code, so you must be able to read it
  • It hints at the next idea: vectorisation

Loops Are Not the Destination

import pandas as pd

prices = pd.Series([10.0, 25.0, 8.5])
prices * 1.23
0    12.300
1    30.750
2    10.455
dtype: float64

No loop. The operation applies to the whole column at once, and it is roughly 100 times faster on real data.

  • You still need loops: for files, for models, for anything not column-shaped
  • But from week 7, if you are writing a loop over rows of a table, there is usually a better way

Your Turn: Comprehensions

temps_c = [12.5, 15.0, 9.8, 21.3, 18.7, 4.2]
  1. Build temps_f converting each to Fahrenheit: f = c * 9/5 + 32
  2. Build a list of temperatures above 15 only
  3. Build a list of labels: "warm" if above 15, "cold" otherwise
  4. Rewrite question 1 as a normal for loop, and check the two give the same result

Solution

temps_c = [12.5, 15.0, 9.8, 21.3, 18.7, 4.2]

temps_f = [c * 9/5 + 32 for c in temps_c]
print(temps_f)

print([c for c in temps_c if c > 15])
print(["warm" if c > 15 else "cold" for c in temps_c])

loop_version = []
for c in temps_c:
    loop_version.append(c * 9/5 + 32)
print(loop_version == temps_f)
[54.5, 59.0, 49.64, 70.34, 65.66, 39.56]
[21.3, 18.7]
['cold', 'cold', 'cold', 'warm', 'warm', 'cold']
True

What We Have Seen

  • if / elif / else, tested in order, first match wins
  • Indentation is part of the language
  • for item in collection: and the accumulator pattern
  • range(), enumerate(), zip()
  • while for “until”, with the infinite loop risk
  • break and continue
  • List comprehensions as a compact loop

References

Thanks for your attention and don’t hesitate to ask if you have any questions! @damien_dupre @damien-dupre https://damien-dupre.github.io damien.dupre@dcu.ie