| Time | Block |
|---|---|
| 0:00 – 0:08 | Warm-up recap |
| 0:08 – 0:38 | 2.1 Comparisons and booleans · 2.2 if / elif / else |
| 0:38 – 0:45 | Break |
| 0:45 – 1:23 | 2.3 for loops · 2.4 Accumulator patterns · 2.5 Looping over dictionaries |
| 1:23 – 1:38 | 2.6 while loops |
| 1:38 – 1:45 | Break |
| 1:45 – 2:05 | 2.7 break/continue · 2.8 List comprehensions · 2.9 try/except |
| 2:05 – 2:25 | 2.10 Day 2 mini-project: Cleaning raw BoC observations |
| 2:25 – 2:30 | Commit 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.
| Operator | Meaning |
|---|---|
== | 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 yourelifs 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. PressCtrl+Cin 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 itembreak→ 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'sexcept Exception as eis 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:
- Loop through the observations. For each, pull out the date (
"d") and the value (obs["V39079"]["v"]). - Skip unusable values with
try/except. - Store clean pairs in two lists:
datesandvalues. - Print how many observations were kept vs dropped.
- Print the latest date and value, and classify the stance using your Day 2
if/elif/elselogic.
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 / elseblocks 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
whileand how to escape an infinite loop - I can skip bad data with
continueandtry/except
git add .
git commit -m "Day 2: control flow and BoC cleaning"
git push