Module 1 · Day 2 of 80

Control Flow: Teaching Python to Make Decisions

About 2.5 hours
Video lesson coming soon
Day 2 · Control Flow
Module 1: Python for Macroeconomics · The recording will appear here as soon as it is published. The written lesson below is available now.

What you'll learn today

  • I can write if / elif / else blocks with correct indentation
  • I can loop over lists, pairs (zip), positions (enumerate), and dictionaries (.items())
  • I can compute period-over-period changes with range
  • I know when to use while and how to escape an infinite loop
  • I can skip bad data with continue and try/except

Today's plan

Timed blocks for the day. Hover a bar for details.

LessonHands-on projectBreak2:30 total
Written lesson
TimeBlock
0:00 – 0:08Warm-up recap
0:08 – 0:382.1 Comparisons and booleans · 2.2 if / elif / else
0:38 – 0:45Break
0:45 – 1:232.3 for loops · 2.4 Accumulator patterns · 2.5 Looping over dictionaries
1:23 – 1:382.6 while loops
1:38 – 1:45Break
1:45 – 2:052.7 break/continue · 2.8 List comprehensions · 2.9 try/except
2:05 – 2:252.10 Day 2 mini-project: Cleaning raw BoC observations
2:25 – 2:30Commit and checklist

Warm-up (8 min)

Without running it, predict the output. Then run it.

rates = [5.00, 4.75, 4.50]
snapshot = {"rate": rates[-1]}
print(f"{snapshot['rate'] - rates[0]:+.2f}")
Answer

-0.50 — last rate (4.50) minus first (5.00), formatted with a sign.

mkdir day02

2.1 Comparisons and Booleans (10 min)

Comparisons produce True or False.

OperatorMeaning
==equal to (two = signs!)
!=not equal to
> <greater / less than
>= <=greater / less than or equal

Example 1 — Simple checks

cpi_yoy = 3.0
target = 2.0

print(cpi_yoy > target)       # True
print(cpi_yoy == target)      # False

Example 2 — Combining conditions with and, or, not

The Bank of Canada aims for 2% inflation within a 1–3% control range.

cpi_yoy = 3.0

in_range = cpi_yoy >= 1.0 and cpi_yoy <= 3.0
print("Inside 1–3% range:", in_range)            # True

# Python allows the neater "chained" form:
print("Inside range:", 1.0 <= cpi_yoy <= 3.0)

spread_bp = -45
unemployment_rising = True
warning = spread_bp < 0 or unemployment_rising
print("Recession warning:", warning)             # True
print("Curve NOT inverted:", not spread_bp < 0)  # False

2.2 if / elif / else (20 min)

Python uses indentation (4 spaces) to show which lines belong to a condition. VS Code inserts them automatically after a colon :.

Example 1 — A single if

Create day02/stance.py:

spread_bp = 148

if spread_bp < 0:
    print("Yield curve is INVERTED — historically a recession signal.")

print("Check complete.")   # not indented → always runs

Example 2 — if / else

real_rate = -0.75

if real_rate < 0:
    print("Real rate is negative: policy is stimulative in real terms.")
else:
    print("Real rate is positive: policy is restrictive in real terms.")

Example 3 — if / elif / else: classifying the policy stance

The notebook shades a "neutral band" of 2.25–2.75% for the overnight rate.

overnight_rate = 2.25

if overnight_rate < 2.25:
    stance = "Stimulative"
elif overnight_rate <= 2.75:
    stance = "Neutral"
else:
    stance = "Restrictive"

print(f"Overnight rate {overnight_rate:.2f}% → {stance}")

Python checks conditions top to bottom and stops at the first True. The order of your elifs matters.

Example 4 — Nested conditions: an inflation briefing

cpi_yoy = 3.0
core_yoy = 2.8

if cpi_yoy > 3.0:
    message = "Above the control range"
elif cpi_yoy < 1.0:
    message = "Below the control range"
else:
    message = "Within the control range"
    if core_yoy > 2.5:
        message += ", but core inflation is elevated"

print(message)

message += "..." is shorthand for message = message + "...".

Try it 2.2

Write code that classifies the unemployment rate: below 5.5 → "Tight", 5.5–7.0 → "Balanced", above 7.0 → "Slack". Test it with 6.4, 5.0, and 8.1.

Solution
unemployment = 6.4   # change this value to test

if unemployment < 5.5:
    labour_market = "Tight"
elif unemployment <= 7.0:
    labour_market = "Balanced"
else:
    labour_market = "Slack"

print(f"Unemployment {unemployment}% → {labour_market}")

2.3 for Loops (20 min)

A for loop repeats code for each item in a collection. Economists constantly do "the same calculation for each period" — that's a loop.

Create day02/loops.py.

Example 1 — Looping over values

rates_2024 = [4.75, 4.50, 4.25, 3.75, 3.25]

for rate in rates_2024:
    print(f"Policy rate: {rate:.2f}%")

Example 2 — Looping with positions: enumerate

for position, rate in enumerate(rates_2024):
    print(f"Decision {position + 1}: {rate:.2f}%")

Example 3 — Pairing dates and values: zip

dates = ["2024-06-05", "2024-07-24", "2024-09-04", "2024-10-23", "2024-12-11"]

for date, rate in zip(dates, rates_2024):
    print(f"{date}: {rate:.2f}%")

Example 4 — Period-over-period changes with range

To compare each value with the previous one we need positions. range(1, n) produces 1, 2, …, n−1.

# Illustrative monthly CPI index levels
cpi = [161.3, 161.8, 162.4, 162.6, 163.0, 163.4,
       163.9, 164.0, 163.7, 164.1, 164.3, 164.2]

for i in range(1, len(cpi)):
    mom = (cpi[i] / cpi[i - 1] - 1) * 100
    print(f"Month {i + 1}: {mom:+.2f}% m/m")

This is exactly what pandas' pct_change() does in the notebook. On Days 4–7 you'll replace this loop with one line — but now you know what it's doing.

Example 5 — Building a new list inside a loop

changes_bp = []                       # start with an empty list

for i in range(1, len(rates_2024)):
    change = (rates_2024[i] - rates_2024[i - 1]) * 100
    changes_bp.append(change)

print(changes_bp)                     # [-25.0, -25.0, -50.0, -50.0]

2.4 Accumulator Patterns (10 min)

Many statistics follow one pattern: start with a value, update it inside the loop.

Example 1 — Counting months above target

cpi_yoy_series = [2.9, 2.6, 2.4, 2.1, 1.9, 2.0, 2.3, 2.5, 3.1, 3.0]

months_above = 0
for value in cpi_yoy_series:
    if value > 2.0:
        months_above += 1

print(f"{months_above} of {len(cpi_yoy_series)} months above target")

Example 2 — Tracking the peak (running maximum)

decisions = [0.50, 1.00, 1.50, 2.50, 3.25, 3.75, 4.25, 4.75, 5.00, 4.75]

peak = decisions[0]
for rate in decisions:
    if rate > peak:
        peak = rate

print(f"Peak policy rate: {peak:.2f}%")    # same answer as max(decisions)

Example 3 — Cumulative change

changes_bp = [25, 50, 50, 100, 75, 50, 50]   # 2022 hikes

cumulative = 0
for change in changes_bp:
    cumulative += change
    print(f"After this hike: +{cumulative} bp cumulative")

2.5 Looping Over Dictionaries (8 min)

Use .items() to get key and value together.

snapshot = {
    "overnight_rate": 2.25,
    "cpi_yoy": 3.0,
    "unemployment": 6.4,
}

for indicator, value in snapshot.items():
    print(f"{indicator:<16} {value:>6.2f}")

Example — Tariff exposure (from the notebook's TARIFF_STATUS)

tariff_exposure = {
    "Automotive (non-CUSMA)": 40,
    "Lumber & wood": 8,
    "Dairy (non-TRQ)": 1.5,
    "Consumer goods (List 3)": 22,
    "Energy": 0,
}

total = 0
for sector, exposure in tariff_exposure.items():
    if exposure > 0:
        print(f"TARIFFED  {sector}: ${exposure}B")
        total += exposure
    else:
        print(f"EXEMPT    {sector}")

print(f"Total exposure: ${total}B")     # $71.5B, matching the notebook KPI

2.6 while Loops (15 min)

A while loop repeats as long as a condition is true. Use it when you don't know in advance how many repetitions you need.

Example 1 — How many years for GDP to double?

growth = 0.02
level = 100.0
years = 0

while level < 200.0:
    level = level * (1 + growth)
    years += 1

print(f"At {growth:.0%} growth, output doubles in {years} years")
print(f"Rule of 70 estimate: {70 / (growth * 100):.0f} years")

Output: the loop gives 36 years; the rule of 70 gives 35. (After 35 years the level is 199.99 — just short!)

Example 2 — How many 25bp cuts to reach neutral?

rate = 5.00
cuts = 0

while rate > 2.75:
    rate -= 0.25
    cuts += 1

print(f"{cuts} cuts of 25bp take the rate to {rate:.2f}%")

Danger — infinite loops: if the condition never becomes False, the loop runs forever. Press Ctrl+C in the terminal to stop it. Always check that something inside the loop moves you toward the exit.

Try it 2.6

Canada's inflation is 3.0% and falls by 0.2 pp each month. Use a while loop to find how many months until it reaches 2.0% or below.

Solution
inflation = 3.0
months = 0

while inflation > 2.0:
    inflation -= 0.2
    months += 1

print(f"{months} months; inflation ends at {inflation:.1f}%")

You might expect 5 months but get 6 — floating-point strikes again (3.0 - 5×0.2 is 2.0000000000000004, still > 2.0). A safer condition is while round(inflation, 4) > 2.0:. This is a real-world lesson: be careful with exact thresholds on decimals.


2.7 break and continue (5 min)

  • continue → skip the rest of this iteration and move to the next item
  • break → exit the loop entirely

Example — Skipping missing values (just like the notebook)

The notebook's boc_fetch skips values like "", "NA", and "Bank holiday". Here's the same logic in plain Python:

raw_values = ["2.25", "2.25", "Bank holiday", "2.25", "", "NA", "2.25"]

clean = []
for v in raw_values:
    if v in ("", "NA", "Bank holiday"):
        continue                     # skip this one
    clean.append(float(v))

print(clean)                         # [2.25, 2.25, 2.25, 2.25]

Example — Finding the first inversion

spreads_bp = [85, 60, 32, 10, -5, -40, -90]
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]

for month, spread in zip(months, spreads_bp):
    if spread < 0:
        print(f"Curve first inverted in {month} ({spread} bp)")
        break

2.8 List Comprehensions — a Gentle Introduction (5 min)

A compact way to build a list from a loop. These two blocks are identical:

rates = [4.75, 4.50, 4.25, 3.75, 3.25]

# Long form
rates_bp = []
for r in rates:
    rates_bp.append(r * 100)

# Comprehension
rates_bp = [r * 100 for r in rates]
print(rates_bp)

With a filter:

cpi_yoy_series = [2.9, 2.6, 2.4, 2.1, 1.9, 2.0, 2.3, 2.5, 3.1, 3.0]
above_target = [x for x in cpi_yoy_series if x > 2.0]
print(above_target)

Use comprehensions for simple transformations. If it doesn't fit comfortably on one line, write a normal loop — readability wins.


2.9 Handling Errors with try / except (10 min)

Real data is messy. Rather than listing every bad value, we can try the conversion and handle failure.

raw_values = ["2.25", "n/a", "2.50", "--", "2.75"]

clean = []
for v in raw_values:
    try:
        clean.append(float(v))
    except ValueError:
        print(f"  Skipped unreadable value: {v!r}")

print(clean)     # [2.25, 2.5, 2.75]

{v!r} shows the value with quotes, so you can spot empty strings or stray spaces.

Good habit: catch the specific error (ValueError), not everything. The notebook's except Exception as e is a broader net used so one failed series doesn't stop the whole pipeline — a deliberate design choice we'll revisit with Prefect retries on Days 54–57.


2.10 Day 2 Mini-Project: Cleaning Raw BoC Observations (20 min)

The Bank of Canada Valet API returns a list of dictionaries shaped like this. (We'll fetch the real thing on Day 3.)

Create day02/clean_boc.py and start with this data:

# Mimics the structure of the BoC Valet API response for series V39079
observations = [
    {"d": "2025-12-01", "V39079": {"v": "2.25"}},
    {"d": "2025-12-02", "V39079": {"v": "2.25"}},
    {"d": "2025-12-25", "V39079": {"v": "Bank holiday"}},
    {"d": "2026-01-02", "V39079": {"v": ""}},
    {"d": "2026-01-05", "V39079": {"v": "2.25"}},
    {"d": "2026-01-06", "V39079": {"v": "NA"}},
    {"d": "2026-01-07", "V39079": {"v": "2.25"}},
]

Tasks:

  1. Loop through the observations. For each, pull out the date ("d") and the value (obs["V39079"]["v"]).
  2. Skip unusable values with try/except.
  3. Store clean pairs in two lists: dates and values.
  4. Print how many observations were kept vs dropped.
  5. Print the latest date and value, and classify the stance using your Day 2 if/elif/else logic.
Solution
series = "V39079"

dates = []
values = []
dropped = 0

for obs in observations:
    date = obs["d"]
    raw = obs[series]["v"]
    try:
        value = float(raw)
    except ValueError:
        dropped += 1
        continue
    dates.append(date)
    values.append(value)

print(f"Kept {len(values)} observations, dropped {dropped}")

latest_date = dates[-1]
latest_rate = values[-1]

if latest_rate < 2.25:
    stance = "Stimulative"
elif latest_rate <= 2.75:
    stance = "Neutral"
else:
    stance = "Restrictive"

print(f"Latest ({latest_date}): {latest_rate:.2f}% → {stance}")

Output: Kept 4 observations, dropped 3 and Latest (2026-01-07): 2.25% → Neutral.

Compare this with boc_fetch in the course notebook — you have just written its core logic by hand.

Stretch: some observations might be missing the series key entirely. Use obs.get(series, {}).get("v", "") to handle that safely (this is exactly what the notebook does).

Day 2 checklist

  • I can write if / elif / else blocks with correct indentation
  • I can loop over lists, pairs (zip), positions (enumerate), and dictionaries (.items())
  • I can compute period-over-period changes with range
  • I know when to use while and how to escape an infinite loop
  • I can skip bad data with continue and try/except
git add .
git commit -m "Day 2: control flow and BoC cleaning"
git push