Try each task in your workspace first. Open a solution only when your code runs, or when you have been stuck for a while. Your solution can be different and still right: compare what it prints. The reading tasks and their answers are on the Questions page.
Writing tasks
Task 6 · from text to a total
A ledger file arrives as lines of text. Loop over the lines, cut each one at the commas with .split(","), convert the pieces, and add up the value.
Show the solution
lines = ["AAPL,10,190.50", "NESN,4,102.30", "VOO,2,512.00"]
total = 0.0
for line in lines:
parts = line.split(",")
total += int(parts[1]) * float(parts[2])
print("Task 6:", total)
It prints:
Task 6: 3338.2
split gives a list of three strings. They are text, so "10" * "190.50" would fail and "10" + "4" would join: convert with int() and float() first (session 2: data from files arrives as text).
Task 7 · the largest position, without max()
Find which position is worth the most, and print its ticker and value. Do not use max(): keep "the best so far" in two names and update them. Loop with zip(tickers, shares).
Show the solution
best_ticker, best_value = "", 0.0
for t, n in zip(tickers, shares):
v = n * price[t]
if v > best_value:
best_ticker, best_value = t, v
print("Task 7:", best_ticker, f"{best_value:.2f}")
It prints:
Task 7: AAPL 1905.00
The pattern "best so far": start below any possible value, look at each item once, replace the best when something beats it. This is what max() does inside; now you can also keep WHICH item was best.
Task 8 · yesterday's lie, fixed
Yesterday, cash_from_file or 1000.0 turned a real balance of 0.0 into 1000.0. Now you have if. Write it so that ONLY a missing value (None) becomes 1000.0, and 0.0 stays 0.0. Try it on 250.0, 0.0 and None.
Show the solution
for cash_from_file in [250.0, 0.0, None]:
if cash_from_file is None:
cash = 1000.0
else:
cash = cash_from_file
print("Task 8:", cash_from_file, "->", cash)
It prints:
Task 8: 250.0 -> 250.0
Task 8: 0.0 -> 0.0
Task 8: None -> 1000.0
Test for the one case you mean. is None (or == None) is true only for a missing value; or treats every "empty" value (0, 0.0, "", []) as missing.
Task 9 · ask before you look up
Loop over the tickers AAPL, MSFT, VOO. For each one, print its price, or "no price" if the dict does not have it. Write it once with in, and once with .get().
Show the solution
wanted = ["AAPL", "MSFT", "VOO"]
for t in wanted:
if t in price:
print("Task 9:", t, price[t])
else:
print("Task 9:", t, "no price")
for t in wanted:
print("Task 9:", t, price.get(t, "no price"))
It prints:
Task 9: AAPL 190.5
Task 9: MSFT no price
Task 9: VOO 512.0
in asks the question first; .get(key, default) asks and answers in one call. Both avoid the KeyError from Task 5. Use in when the two cases need different code, .get when you only need a fallback value.
Task 10 · how many years until 1500?
1000.00 CHF grows by 3 % a year. After how many whole years is it worth at least 1500.00? Nobody knows the number of passes in advance, so use while. Print the years and the value then.
Show the solution
value = 1000.0
years = 0
while value < 1500.0:
value *= 1.03
years += 1
print("Task 10:", years, round(value, 2))
It prints:
Task 10: 14 1512.59
The body must move the condition towards False (value grows), or the loop never ends. Counting in the same body gives the number of passes. 500 / 30 = 16.7 would be the answer without compounding.
★ Harder: classic puzzles
★ Task 11 · Running total, and the worst moment
Daily profit and loss of a portfolio, in CHF. Build the list of running totals after each day, and find the lowest the running total ever got.
Show the solution
pnl = [120.0, -340.5, 80.25, -60.0, 410.0, -95.75]
running, worst = 0.0, 0.0
totals = []
for x in pnl:
running += x
totals.append(running)
if running < worst:
worst = running
print("Task 11:", totals)
print("Task 11: worst", worst)
It prints:
Task 11: [120.0, -220.5, -140.25, -200.25, 209.75, 114.0]
Task 11: worst -220.5
Two accumulators in one loop: a running sum, and a running minimum of that sum. The worst DAY (-340.5) is not the worst MOMENT (-220.5): the question asks about the balance, not the day.
★ Task 12 · The trade that appears twice
A list of trades in the order they arrived. Count how many times each ticker appears (a dict, with .get()), and print the FIRST ticker that shows up a second time.
Show the solution
trades = ["AAPL", "NESN", "VOO", "NESN", "AAPL"]
count = {}
first_twice = None
for t in trades:
count[t] = count.get(t, 0) + 1
if count[t] == 2 and first_twice is None:
first_twice = t
print("Task 12:", count, first_twice)
It prints:
Task 12: {'AAPL': 2, 'NESN': 2, 'VOO': 1} NESN
count.get(t, 0) + 1 counts without a KeyError on the first sighting. The first ticker to reach 2 is NESN (position 3), even though AAPL came first in the list and also appears twice.
★ Task 13 · Two sum: spend the budget exactly
One share each of two DIFFERENT stocks must cost exactly 614.30 CHF. Which two? Prices are in cents, so == is safe. Step 1: two loops, one inside the other, so that every pair is tried once. Step 2 (★★): one loop only, with enumerate(). Keep a dict of the prices already seen; for each price, ask "have I seen the price that completes it?".
Show the solution
all_tickers = ["AAPL", "NESN", "VOO", "MSFT", "NOVN"]
all_prices_c = [19050, 10230, 51200, 41000, 8870]
budget_c = 61430
for i in range(len(all_prices_c)):
for j in range(i + 1, len(all_prices_c)):
if all_prices_c[i] + all_prices_c[j] == budget_c:
print("Task 13:", all_tickers[i], all_tickers[j])
seen = {} # price -> its position in the list
for i, p in enumerate(all_prices_c):
need = budget_c - p
if need in seen:
print("Task 13:", all_tickers[seen[need]], all_tickers[i])
seen[p] = i
It prints:
Task 13: NESN VOO
The nested loops try every pair: j starts at i + 1, so no pair twice and no stock with itself. The one-loop version remembers what it has seen: for 51200 it needs 10230, which is already in the dict. For 5 stocks both are instant; for a million, the dict version is the only one that finishes.
★ Task 14 · Best day to buy, best day to sell
The daily closes of AAPL for a week (day 0 to day 6). You may buy once and sell once, later. What is the largest profit, and on which days? One loop with enumerate() is enough: remember the lowest price so far, and ask every day "what if I sold today?".
Show the solution
low, low_day = closes[0], 0
best, buy, sell = 0.0, 0, 0
for day, c in enumerate(closes):
if c - low > best:
best, buy, sell = c - low, low_day, day
if c < low:
low, low_day = c, day
print(f"Task 14: buy day {buy}, sell day {sell}, profit {best:.2f}")
It prints:
Task 14: buy day 3, sell day 5, profit 13.50
Buy at the lowest price BEFORE the sale, not the lowest of the week overall. The order of the two ifs matters: check "sell today" against the low so far, then update the low. Swapping them would let you buy and sell on the same day.
★ Task 15 · numpy: slicing in two directions
Pavel's slices.py showed a numpy table: one row per position, column 0 = shares, column 1 = price. With numpy, [rows, columns] slices both directions at once. On paper, predict all five lines, then run and check. What does the last line compute, and why does it not need a loop?
Show the solution
import numpy as np
table = np.array([[10, 190.5], [4, 102.3], [2, 512.0]]) # rows: AAPL, NESN, VOO
print("Task 15:", table[1:, 0]) # shares of NESN and VOO
print("Task 15:", table[:2, 1]) # prices of AAPL and NESN
print("Task 15:", table[-1]) # the whole VOO row
print("Task 15:", table[::2, 1]) # prices of every other row: AAPL, VOO
print("Task 15:", table[:, 0] * table[:, 1]) # all values at once
It prints:
Task 15: [4. 2.]
Task 15: [190.5 102.3]
Task 15: [ 2. 512.]
Task 15: [190.5 512. ]
Task 15: [1905. 409.2 1024. ]
Before the comma: which rows; after it: which columns; each part is an ordinary slice. The shares print as 4. and 2. because a numpy array holds one type, and one price is a float. The last line multiplies the two columns element by element: numpy does the loop for you, in fast code. Session 5 goes much further.
Extra practice
Practice 1 · The second-highest close, in one pass
Find the highest and the second-highest close of the week in ONE loop, without sorted() or max(). Keep two names, best and second, and decide for each price which one it replaces.
Show the solution
best = second = float("-inf")
for c in closes:
if c > best:
best, second = c, best
elif c > second:
second = c
print(best, second)
It prints:
198.6 194.0
When a new best arrives, the old best becomes the second. float("-inf") is smaller than every number, a safe start. Sorting would work too, but it does far more work than needed.
Practice 2 · Remove duplicates, keep the order
Build a new list with each ticker once, in the order it FIRST appeared. Use a dict to remember what you have seen.
Show the solution
trades = ["AAPL", "NESN", "VOO", "NESN", "AAPL", "MSFT"]
seen = {}
unique = []
for t in trades:
if t not in seen:
seen[t] = True
unique.append(t)
print(unique)
It prints:
['AAPL', 'NESN', 'VOO', 'MSFT']
Asking t not in seen on a dict is instant however long the list is; asking t not in unique would search the list every time. The order comes from the list, the memory from the dict.
Practice 3 · The longest rising streak
A rising streak is a run of days where each close is higher than the day before. How many days long is the longest streak in closes (a single day counts as a streak of 1)?
Show the solution
run = longest = 1
for i in range(1, len(closes)):
if closes[i] > closes[i - 1]:
run += 1
else:
run = 1
if run > longest:
longest = run
print(longest)
It prints:
3
Compare each day with the one before, so the loop starts at 1. The current run resets on a fall; the longest run only grows. The streak is 185.1 → 191.4 → 198.6.
Practice 4 · Merge two holdings
Two accounts hold shares: a = {"AAPL": 10, "NESN": 4} and b = {"NESN": 6, "VOO": 2}. Build ONE dict with the total shares per ticker, and leave a unchanged (check it at the end).
Show the solution
a = {"AAPL": 10, "NESN": 4}
b = {"NESN": 6, "VOO": 2}
merged = a.copy()
for ticker, n in b.items():
merged[ticker] = merged.get(ticker, 0) + n
print(merged, a)
It prints:
{'AAPL': 10, 'NESN': 10, 'VOO': 2} {'AAPL': 10, 'NESN': 4}
Without .copy(), merged would be a second name for a, and a would change too (Task 2). .items() gives key and value together; .get(ticker, 0) handles tickers that only b has.