You will use range all the time: it is how a loop counts. range(3) gives 0, 1, 2, exactly the positions of a 3-item list:
| You write | Gives | Like slicing |
|---|---|---|
range(3) | 0, 1, 2 | stop not included |
range(1, 4) | 1, 2, 3 | start, stop |
range(0, 10, 3) | 0, 3, 6, 9 | start, stop, step |
range(len(tickers)) fits a list of any length.
On the left, line 8 now loops over the items: ticker is "AAPL", then "NESN", then "VOO".
That is the simplest loop, but line 9 still asks for shares[i], and there is no i any more: NameError.
for ticker in tickers: you get the items. Use it when one list is enough.for i in range(len(tickers)): you get the positions. Our script needs them: line 9 uses the same position in two lists.Session 1's tracing table, one row per pass:
| pass | i | tickers[i] | value | total after line 10 |
|---|---|---|---|---|
| before | — | — | — | 0.0 |
| 1 | 0 | "AAPL" | 1905.0 | 1905.0 |
| 2 | 1 | "NESN" | 409.2 | 2314.2 |
| 3 | 2 | "VOO" | 1024.0 | 3338.2 |
After the third pass there are no more values. The loop ends, and total holds the sum.
Three steps, always the same shape:
0 (line 7).You will write this pattern hundreds of times: totals, counts, averages.
The same shape, with a condition inside:
tickers = ["AAPL", "NESN", "VOO"]
values = [1905.0, 409.2, 1024.0]
count = 0
large = []
for i in range(len(values)):
if values[i] >= 1000:
count += 1
large.append(tickers[i])
print(count, large)
2 ['AAPL', 'VOO']
Counting: start at 0, add 1. Collecting: start with [], .append each match.
Keep the best so far, and replace it when you meet something bigger:
values = [1905.0, 409.2, 1024.0]
best = values[0]
for v in values:
if v > best:
best = v
print(best, max(values))
1905.0 1905.0
Python's max() does the same in one call. Write the loop once to see how it works; then use max().
The inner loop runs completely, for every pass of the outer loop:
tickers = ["AAPL", "NESN", "VOO"]
passes = 0
for ticker in tickers:
for year in [1, 2]:
passes += 1
print(passes)
6
3 tickers × 2 years = 6 passes of the inner body. Our if chain inside the for is the same idea: a block inside a block.
In most languages, indentation is only for the reader. In Python it is grammar: the indent decides which block a line belongs to, and so how the program runs.
On the left, line 17 has lost four spaces. It left the loop body, so it runs once, after the loop, with the values of the last pass: only VOO is printed.
No error. Same words, different program. Session 2 said blocks are indentation; here it changes the result.
Rule: indent with 4 spaces, and let the editor do it.
Line 11 lost its :. Look at the output: not even line 5 ran. Python reads the whole file before running anything, so a grammar mistake stops everything (session 1: it won't start).
Every for, while, if, elif and else line ends with :.
Now line 12 is not indented under the if on line 11. The if has an empty block, and Python refuses to start.
After every :, the block must be indented. Editors do it for you when you press Enter after a :.
Part 3
Let the program choose which lines run.
value = 409.2
if value >= 1500:
print("large")
print("done")
done
value >= 1500 is a bool (session 2). If it is True, the indented block runs. If False, Python skips it.
Here it is False: only done is printed.
if: check the first condition.elif ("else if"): only if everything above was False, check this one.else: only if everything above was False. No condition.Exactly one block of the chain runs: the first one whose condition is True, or else.
Output lines 2–4: one label per position, never two.
For VOO, value is 1024.0:
The first True wins. Nothing after it in the chain is checked.
Lines 8–17 have two levels. The if chain sits inside the loop, so the whole chain runs once per position:
Blue: runs once per pass. Gold: runs only when its condition is chosen. Read the indentation first; it shows you the structure before the words do.
On the left, the two checks are swapped. 1905 is also >= 500, so the first check wins and large is never reached: AAPL is labelled medium.
No error. A wrong label, forever.
Rule: in a chain of thresholds, test the biggest first.
A position worth exactly 1500:
value = 1500.0
print(value >= 1500, value > 1500)
True False
With >= it is large. With > it is medium. Both run; both look fine.
Which one is right? The brief decides, not the code. When a rule says "above 1500", ask: does 1500 itself count?
if also accepts values that are not bool. Python asks: is it empty, or zero?
print(bool([]), bool(["AAPL"]), bool(0), bool(""))
False True False False
So if large: means "if the list is not empty". You will see this often in other people's code, and in AI-written code.
Our course writes the full question: if len(large) > 0:. Slower to type, faster to read.
Part 4
Look a value up by its name, not by its position.
Curly braces, and key: value pairs:
price["NESN"] gives 102.3. Same square brackets as a list, but inside goes a key, not a position.
| You need… | Use | Look up with |
|---|---|---|
| items in order, by position | list | shares[1] |
| a value for each name | dict | price["NESN"] |
Our script has both. shares works only because it is in the same order as tickers. price needs no order at all: every price is attached to its name.
Rule of thumb: if you find yourself writing "the position of NESN", you wanted a dict.
price[tickers[i]] has brackets inside brackets. Start with the innermost. In the second pass, i is 1:
The list turns a position into a name. The dict turns the name into a price.
Line 5 now looks up "MSFT", which is not a key of price. The program stops with a KeyError, which names the missing key.
A list complains about a position (IndexError); a dict complains about a key (KeyError).
Two safe ways to handle a key that may be missing, on line 5:
"MSFT" in price checks the keys: False. Like ==, in is an operator that asks a yes/no question, so its answer is a bool: you can use it directly in an if.price.get("MSFT", 0.0) returns the default instead of crashing: 0.0.Is 0.0 a sensible price? The brief decides. Sometimes a crash is the honest answer.
Read it the session 2 way: last line first, KeyError: 'MSFT'. Then the line: 9. Then the piece: the carets point at price[tickers[i]].
New today: a loop runs line 9 many times. Which pass failed? The output before the traceback tells you: three positions were printed, so the fourth pass crashed.
The fix belongs in the data, not the loop: add a price for MSFT.
Assign to a key. Existing key: the value is replaced. Line 5 now sets NESN's price to 103.0, and the loop uses the new price.
New key: a new entry. price["MSFT"] = 410.0 would add MSFT.
Each key appears once. A dict cannot hold two prices for NESN.
A for loop over a dict gives you its keys, in the order they were added:
price = {"AAPL": 190.5, "NESN": 102.3, "VOO": 512.0}
for ticker in price:
print(ticker, price[ticker])
AAPL 190.5 NESN 102.3 VOO 512.0
With the key in hand, price[ticker] gives the value.
A tuple is like a list in round brackets, but it cannot change. Use it for a fixed group, such as one position:
pos = ("NESN", 4)
ticker, n = pos
print(pos, pos[0], ticker, n, type(pos))
('NESN', 4) NESN NESN 4 <class 'tuple'>
ticker, n = pos is unpacking: two names on the left take the two values, in order. You will meet tuples mostly this way, in the next slides.
price = {"AAPL": 190.5, "NESN": 102.3, "VOO": 512.0}
print(list(price.keys()))
print(list(price.values()))
for ticker, p in price.items():
print(ticker, p)
['AAPL', 'NESN', 'VOO'] [190.5, 102.3, 512.0] AAPL 190.5 NESN 102.3 VOO 512.0
.items() gives (key, value) tuples. Unpacking them in the loop header gives you both at once: no price[ticker] needed.
You will see these in almost every real script, and in AI-written code:
for ticker, n in zip(tickers, shares): walks two lists side by side, like a zipper.for i, ticker in enumerate(tickers): gives the position and the item.Both hand out tuples, unpacked in the loop header. Reading them is enough for now.
| You write | What it does |
|---|---|
d[key] | the value for key; KeyError if missing |
d[key] = v | add a new entry, or replace the value |
key in d | True if key is a key |
d.get(key, default) | the value, or default if missing |
len(d) | the number of entries |
for key in d: | go through the keys, in the order they were added |
Compare with the list: a dict has no position, no slicing, no +. Different type, different operations.
One dict per position, all in a list:
ledger = [{"ticker": "AAPL", "shares": 10, "price": 190.5},
{"ticker": "NESN", "shares": 4, "price": 102.3},
{"ticker": "VOO", "shares": 2, "price": 512.0}]
print(ledger[1]["price"])
102.3
A row is a dict, the table is a list. Part 6's DataFrame stores the same thing, built for speed.
| Brackets | Right after a name | On their own |
|---|---|---|
( ) | a call: print(x), len(a) | grouping: (value + cash) |
[ ] | look up: tickers[0], price["NESN"] | a new list: [10, 4, 2] |
{ } | — | a new dict: {"AAPL": 190.5} |
When you read code, the brackets tell you what is happening before you read the names.
Part 5
Repeat until a condition stops being true.
Question: at 5% a year, how many years until the ledger is worth twice as much?
True: run the body (lines 22–23), then check again.False: the loop ends, and Python continues at line 24.Nobody knows in advance how many passes it takes. That is exactly when you use while.
The target is 2 * total = 6676.4:
years | grown | grown < 6676.4? |
|---|---|---|
0 | 3338.2 | True: run the body |
1 | 3505.11 | True |
2 | 3680.37 | True |
| … | … | … |
14 | 6609.41 | True |
15 | 6939.88 | False: stop |
(grown rounded to cents here; the loop keeps full floats.)
for |
while |
|
|---|---|---|
| Repeats | once per item | as long as a condition is True |
| Number of passes | known: the items | unknown in advance |
| Line in our script | 8 | 21 |
| Stops | by itself | only when the body makes the condition False |
Most loops in data work are for loops. Reach for while when the question is "until when?"
If the body never makes the condition False, the loop runs forever:
grown = 3338.2
while grown < 6676.4:
years += 1 # forgot: grown *= 1.05
The program hangs. No error, no output. Stop it with Ctrl+C in the terminal, or the stop button (■) in VS Code.
Before running a while loop, find the line in the body that moves the condition towards False. Line 22 is ours.
tickers = ["AAPL", "NESN", "VOO"]
for ticker in tickers:
print("checking", ticker)
if ticker == "NESN":
print("found it")
break
checking AAPL checking NESN found it
break ends the loop at once, even with items left. VOO is never checked.
Useful for searching: stop as soon as you have found what you need.
Part 6
numpy and pandas: the types data science is built on.
Session 1: Python's strength is its libraries. Many of them bring new types, each with its own operations.
On the left, the same ledger, with two libraries:
import numpy as np loads the library under a short name. Session 4 explains import; today, just read it as "use numpy".
An array's * works element by element: position 0 with position 0, and so on. Lines 7–10 of ledger.py in one line, with no loop.
values.sum() is a method of the array type: 3338.2.
| You write | Type | You get | What * means |
|---|---|---|---|
3 * 2 | int | 6 | multiply |
"3" * 2 | str | "33" | repeat the text |
[10, 4, 2] * 2 | list | [10, 4, 2, 10, 4, 2] | repeat the list |
shares * 2 | numpy array | [20 8 4] | multiply each element |
[10, 4, 2] and np.array([10, 4, 2]) look almost the same. Their * does something completely different. Before you read an operator, find out the type.
A list can mix types. A numpy array cannot: it converts every element to one type. Line 9 now builds two mixed arrays.
First array: the ints became floats, like session 2's ladder. Second array: the 10 became the text '10'. numpy coerces even to text, which plain Python never does. One more reason to check the type of your data.
Line 9 now compares the whole array with 1000, then keeps the matches:
The "collect" pattern from Part 2, with no loop: compare, then keep what is True.
A DataFrame is a table: columns with names, rows numbered from 0.
ledger["price"].Lists, dicts, element-by-element arithmetic: today's ideas, combined into one type. Part 2 of the course is built on it.
type() works on library values too: shares is a numpy.ndarray, and ledger is a pandas DataFrame.
The library name is part of the type's name: it tells you whose operations you are using. When * or + surprises you, type() is the first question.
Older pandas versions print the longer name pandas.core.frame.DataFrame. Same type.
shares * prices replaces a loop. Less code, fewer places for a bug.The price: new types with their own rules. + on a list joins; + on an array adds. You now know to check.
A classmate swapped lines 7 and 8: total = 0.0 now sits inside the loop.
It runs. Every line prints. The total looks like money.
Is it right? Which kind of failure is this, and how would you catch it?
total = 0.0 is inside the loop, so it runs on every pass. Each pass throws the running total away. Only the last position survives: VOO, 1,024.00.
Catch it with a rough check: the total must be at least as big as the largest position. AAPL alone is worth 1,905.
The fix: start the accumulator before the loop, as line 7 does. Accumulator pattern, step 1.
| Type | Example | Look up by | Can change? | + means |
|---|---|---|---|---|
str | "NESN" | position | no | join |
list | [10, 4, 2] | position | yes | join |
tuple | ("NESN", 4) | position | no | join |
dict | {"NESN": 102.3} | key | yes | TypeError |
| numpy array | np.array([10, 4, 2]) | position | yes | add each element |
pandas DataFrame | the ledger table | column name | yes | column by column |
Six types, one question to ask of each: what do its operations mean?
The same 24 lines as at the start. Every line is now yours:
On paper first, then check with your neighbour:
tickers[1:3]? And shares[-1]?shares.append(5), what is len(shares)?500.0: which size does line 11–16 give it?[1, 2] + [3]? And np.array([1, 2]) + np.array([3, 4])?while loop run?[start:stop], stop not included.for, and use the accumulator, counting and collecting patterns.if / elif / else, biggest threshold first.while, and make sure it stops.* and + mean for int, str, list and a numpy array.Next session: Monday 5 October, functions and imports. The ledger script gets organised into functions.
GENERATED from slides.src.md by tools/codewalk/build.py. Edit the source, not this file.