Chapter 0 — Basics of Python

Learning Statistics with Python

Prof. Xuhu Wan

Chapter 0 · Learning Statistics with Python

Basics of Python

Python as a calculator, numbers, strings, lists, tuples, sets, dictionaries, control flow, comprehensions, functions, error handling.

Prof. Xuhu Wan

ISOM, HKUST Business School · 2026 Edition

What This Chapter Builds

  • The four scalar types (int, float, bool, str) and the arithmetic traps between them.
  • The four containers (list, tuple, set, dict) — which ones you can change, which ones you can index, and what “the same object” means.
  • if / for / while / break / continue, then the one-line loop: the list comprehension.
  • Functions with defaults, *args, **kwargs, lambda, scope, and try / except.
  • A closing mini-project — word counts from raw text — using all of it at once.

Why this matters

Every pandas call in Chapters 2–7 is a method on an object, takes keyword arguments, and returns a container. If df.loc[...], key=lambda …, **kwargs, or a KeyError still looks like magic, the statistics will too. This chapter makes them boring.

Roadmap

Section Concept Tool
1.1 Python as a calculator; numbers, booleans, strings / vs //, type(), slicing s[a:b:c], string methods
1.2 Containers and the mutability / aliasing rules list, tuple, set, dict, .copy(), is vs ==
1.3 Control flow and list comprehensions if/elif/else, for, while, break, continue, [… for … if …]
1.4 Functions, lambdas, f-strings, error handling def, defaults, *args/**kwargs, lambda, f"{x:.2f}", try/except

Python as a Calculator: Numbers, Booleans, Strings

You already know arithmetic. What you will predict here is where Python’s arithmetic differs from the calculator on your desk — one division operator that always returns a float, one that rounds down, a True that is secretly 1, and a text type that behaves like a list you cannot edit.

Start with the obvious

Python evaluates expressions top to bottom and shows the value of the last one. Predict the second line.

What does this print?

print(1 + 2)
print((3 - 1) * 5 / 2)

3 then 5.0

3, 5.0, 1024, 1. The .0 on the second line is the first trap: / always returns a float, even when the answer is a whole number.

Two divisions, not one

/ is true division; // is floor division. Commit to an answer for the negative case before running.

What does print(10 // 3, -10 // 3, 10 % 3) print?

  • 3 -3 1
  • 3.0 -3.0 1
  • 3 -4 1
  • 3.33 -3.33 1

Confirm the floor

3.3333333333333335, then 3 -4 1. Floor division rounds down-3.33 floors to -4. In finance you will use // to count whole lots (shares // 100) and % for the odd lot (shares % 100); get the sign wrong on a short position and the lot count is off by one.

int, float, and what type() tells you

Mixing an int with a float promotes the result to float (45.14159). Python never silently truncates — but floats have their own trap, next slide.

Is 0.1 + 0.2 equal to 0.3?

What does print(0.1 + 0.2 == 0.3) print?

  • True
  • False
  • It raises a TypeError
  • 0.3

False, 0.30000000000000004, True. Every price, return and p-value in this course is a float. Never test them with ==.

Booleans are integers in disguise

True is 1, False is 0. Predict the sum.

What does the last line print?

passISOM5000 = True     # True = 1
passISOM6000 = False    # False = 0
print(passISOM5000 + passISOM6000)

1

1, then <class 'bool'> 2. Because booleans add, sum(returns > 0) counts the up-days in Chapter 1 — a bool is a subclass of int.

Strings are sequences: index and slice them

s[start:stop:step] — start inclusive, stop exclusive. Predict two of the six lines.

With s = ‘data Analysis’, what are s[0:4] and s[::2]?

  • 'data' and 'dt nlss'
  • 'data ' and 'dt nlss'
  • 'data' and 'aaAayi'
  • 'dat' and 'dt nlss'

Run the six slices

d, s, data, data, Analysis, dt nlss. Exactly the same rule slices lists, tuples, Series and DataFrames — df.iloc[0:4] is four rows, not five.

Objects have methods: object.method(args)

Every value in Python is an object, and each object carries functions that belong to it. You call them with a dot.

split is the one to remember: it turns one string into a list of strings — the first step of the text mini-project in §0.4.

Did s.upper() change s?

After s = ” hello, world “; s.upper(), what does print(s) show?

  • ' HELLO, WORLD '
  • ' hello, world '
  • None
  • It raises an error

' hello, world ', then ' HELLO, WORLD '. This is the pandas habit too: df.dropna() returns a new frame; the old one is unchanged until you assign.

Your turn: clean a string, raise a power

Turn s into 'HELLO, WORLD' (strip the spaces, then upper-case — chain two methods), and set power to \(2^{10}\) using the exponentiation operator, not *.

What you discovered

  • / always returns a float; // floors toward \(-\infty\) (-10 // 3 is -4); % is the remainder.
  • 0.1 + 0.2 != 0.3 — compare floats with a tolerance, never ==.
  • True + True is 2: booleans are integers, which is why sum(condition) counts.
  • Slices are start-inclusive, stop-exclusive, and the same rule runs through lists and DataFrames.
  • Strings are immutable: s.upper() returns a new string; keep it with s = s.upper().

Next: §0.2 — containers, and the difference between a copy and a second name for the same object.

Containers: Lists, Tuples, Sets, Dictionaries

Four ways to hold many values. You will predict which ones can be edited in place, which ones have positions, and — the trap that bites every analyst once — what happens to a when you change b.

Lists: ordered, mutable, mixed types

Predict the first line before you run.

nums = [10, 20, 30, 40]. What does print(nums[1:-1]) show?

print(nums[1:-1])

[20, 30]

[20, 30][10, 20, 30, 40, 50][10, 15, 20, 30, 40] removed: 5010 40 [15, 20]. append, insert, pop all edit the list in place — no assignment needed.

The aliasing trap

Two names, one list. Predict before running — this one costs people real money.

nums = [10, 20, 30, 40]; alias = nums; alias.append(50). What is nums now?

  • [10, 20, 30, 40]
  • It raises an error — nums is protected
  • [10, 20, 30, 40, 50]
  • [50, 10, 20, 30, 40]

Alias vs copy

nums: [10, 20, 30, 40, 50] alias: [10, 20, 30, 40, 50] then nums: [10, 20, 30, 40, 50] copy: [10, 20, 30, 40, 50, 15]. Assignment never copies; .copy() does. In Chapter 2 this returns as the SettingWithCopyWarning.

is asks about identity; == asks about value

nums = [10, 20, 30, 40]; alias = nums; fresh = [10, 20, 30, 40]. What do nums == fresh, nums is fresh, alias is nums give?

  • True True True
  • True False True
  • False False True
  • True False False

True False True. Use is only for None (x is None); use == for everything else.

Tuples: ordered and immutable

Parentheses instead of brackets, and one difference that matters. Predict.

coords = (34.05, -118.25); coords[0] = 0. What happens?

  • coords becomes (0, -118.25)
  • coords becomes (0,)
  • A ValueError
  • A TypeError — tuples do not support item assignment

See the error, then unpack

TypeError: 'tuple' object does not support item assignment. Unpacking (lat, lon = coords) is how you will receive the (sharpe, max_drawdown) pair from PerformanceMeasure in the projects.

Sets: unique, unordered

bag = {1, 2, 2, 3, 3, 3} — what does print(bag, len(bag)) show?

print(bag, len(bag))

{1, 2, 3} 3

{1, 2, 3} 3, True, {1, 2, 3, 4, 5}, {2}, {1, 2, 3}. set(tickers) is the one-line answer to “how many distinct tickers are in this file?”

Can you take the first element of a set?

What does bag[0] do for a set bag?

  • Returns the smallest element
  • Returns the first element inserted
  • Raises TypeError: 'set' object is not subscriptable
  • Returns 0

Dictionaries: key → value

A pandas Series is a dictionary that learned arithmetic; a DataFrame is a dictionary of columns. .keys(), .values(), .items() return in insertion order (Python ≥ 3.7).

Ask for a key that isn’t there

Before “gpa” is added, student = {“name”: “Alex”, “age”: 20, “skills”: […]}. What do student[“gpa”] and student.get(“gpa”, 0.0) do?

  • Both return None
  • The first raises KeyError; the second returns 0.0
  • Both raise KeyError
  • The first returns 0.0; the second raises KeyError

KeyError: 'gpa', then 0.0, then None. When df["Volumn"] throws a KeyError in Chapter 2, it is this exact mechanism — and the fix is a typo, not a .get.

Attributes vs methods

  • Attribute — a property you read: my_list.__class__, df.shape, df.columns.
  • Method — a function you call with (): my_list.append(3), text.upper(), df.head().

text = “tiger”. What is text.isalpha (no parentheses)?

  • True
  • False
  • The method object itself — not yet called
  • A SyntaxError

<class 'list'>, True, True, builtin_function_or_method. Rule for pandas: df.shape and df.columns (no brackets); df.head() and df.mean() (brackets). Forget the brackets and you print <bound method …> instead of a number.

Your turn: slice, de-duplicate, summarise

The notebook’s mini-exercise. From the list of 5 numbers nums5: (1) slice the middle three into middle3; (2) convert it to a set — what happens to the duplicate 20?; (3) build stats5 = {'min': …, 'max': …} with min() and max().

What you discovered

  • Lists edit in place (append, insert, pop); tuples refuse (TypeError); strings refuse too.
  • alias = nums is a second name, not a copy — use .copy() when you want independence.
  • == compares values, is compares identity; reserve is for None.
  • Sets collapse duplicates and have no positions (bag[0] fails); sorted(bag) gives you a list.
  • d[key] raises KeyError on a missing key; d.get(key, default) does not.
  • Attributes are read, methods are called — df.shape vs df.head().

Next: §0.3 — making decisions and repeating them: if, for, while, and the comprehension.

Control Flow and List Comprehensions

Programs branch and repeat. You will predict how elif order changes a grade, why range(1, 5) has four numbers, what continue skips — and then compress a four-line loop into one line that reads like set-builder notation.

if / elif / else

With score = 59, which branch fires?

if score >= 90:   grade = "A"
elif score >= 80: grade = "B"
elif score >= 60: grade = "C"
else:             grade = "D or below"

D or below

59 D or below. Indentation is the block — four spaces, no braces. Python stops at the first branch that is True.

Order of branches matters

score = 95. if score >= 60: grade = “C” then elif score >= 90: grade = “A”. What is grade?

  • "C" — the first true branch wins
  • "A" — the best matching branch wins
  • Both run; grade ends as "A"
  • A SyntaxError — thresholds must be increasing

for loops and enumerate

Indices start at 0. enumerate(cities, 1) starts the counter at 1 — useful for printing ranks. You never write for i in range(len(cities)) in idiomatic Python.

How many numbers in range(1, 5)?

What does list(range(1, 5)) return?

  • [1, 2, 3, 4, 5]
  • [1, 2, 3, 4]
  • [0, 1, 2, 3, 4]
  • [1, 5]

while: repeat until a condition fails

n = 5; total = 0 and while n > 0: total += n; n -= 1. What is total?

print(total)

15

Warning

Forget n = n - 1 and the loop never ends. Use while only when you do not know the number of iterations in advance (e.g. “keep bidding until the order fills”); otherwise use for.

break stops; continue skips

for x in range(1, 11): if x % 3 == 0: continue; print(x, end=” “). Output?

  • 1 2
  • 3 6 9
  • 1 2 4 5 7 8 10
  • 1 2 3 4 5 6 7 8 9 10

Run both

First multiple of 7: 7 and 1 2 4 5 7 8 10. break is how a search loop exits early; continue is how a cleaning loop discards bad rows without nesting the whole body in an if.

Your turn: FizzBuzz

Fill the list fizz for 1..20: multiples of 3 become "Fizz", multiples of 5 "Buzz", multiples of both "FizzBuzz", everything else the number as a string. Test the “both” case first.

List comprehensions

[new_item for item in iterable if condition] — a loop that builds a list in one expression. Predict what each variant produces before you run it.

The loop and its one-line twin

What does [x**2 for x in range(0, 6)] return?

squares = [x**2 for x in range(0, 6)]
print(squares)

[0, 1, 4, 9, 16, 25]

Add a filter

What does [x**2 for x in range(10) if x % 2 == 0] return?

  • [0, 4, 16, 36, 64]
  • [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
  • [1, 9, 25, 49, 81]
  • [0, 4, 16, 36, 64, 100]

if after for filters; if … else before for labels

How many elements does [“even” if x % 2 == 0 else “odd” for x in range(6)] have?

  • 3
  • 0
  • 6 — every element gets a label
  • It is a SyntaxError

['even', 'odd', 'even', 'odd', 'even', 'odd'] and ['NYC', 'LA', 'CHICAGO']. The label pattern becomes np.where(cond, "even", "odd") in Chapter 1 — same idea, vectorised.

Your turn: filter, then map

Mirror the even_squares example: build odd_cubes, the cubes of the odd numbers in range(10), as one comprehension. Expected: [1, 27, 125, 343, 729].

What you discovered

  • Python takes the first true branch — order if/elif from strictest to loosest.
  • range(1, 5) is 1, 2, 3, 4: the same stop-exclusive rule as slicing.
  • enumerate gives (index, value); for i in range(len(x)) is a smell.
  • break exits the loop; continue skips to the next iteration; while needs a guaranteed exit.
  • [f(x) for x in xs if cond] filters then maps; [a if c else b for x in xs] labels every element.

Next: §0.4 — wrap the logic in functions, format the output, and survive bad input.

Functions, Lambdas, f-strings, and Error Handling

A function is a named, reusable block with its own scope. You will predict how defaults, *args, and **kwargs fill in, when a lambda earns its place, why a variable set inside a function vanishes outside, and how try/except turns a crash into a value. Then the mini-project.

Define, document, call

def show(x): print(x) then r = show(5). What is r?

  • 5
  • None
  • "5"
  • A TypeError

Default arguments

What do the two calls print?

def greet(name, title="Student"):
    return f"Hello {title} {name}!"
print(greet("Alex")); print(greet("Alex", title="Dr."))

Hello Student Alex! then Hello Dr. Alex!

Defaults are why pd.read_csv(path) works with one argument and pd.read_csv(path, sep=";", index_col=0) with three. You expose sensible defaults and let callers override.

The mutable-default trap

def add_city(c, cities=[]): cities.append(c); return cities. After add_city(“NYC”), what does add_city(“LA”) return?

  • ["LA"]
  • []
  • ["NYC", "LA"]
  • A TypeError

*args: any number of positional values

mean(first, *nums) averages first with the rest. What is mean(10, 20, 30, 40, 100)?

print(mean(10, 20, 30, 40, 100))

40.0

10.0 and 40.0. Requiring first guarantees at least one value, so the division can never be by zero. print(...) itself is a *args function — that is why it takes any number of arguments.

**kwargs: any number of keyword options

Connecting to db.local:5432, ssl=False then Connecting to db.local:6500, ssl=True. **options is a dictionary, so .get(key, default) from §0.2 is exactly the tool. This is how df.plot(**plot_kwargs) forwards styling to matplotlib.

Putting the signature together: order matters

def func(required, defaulted=…, *args, kw_only=…, **kwargs). Predict the middle call.

demo(a, b=10, *args, scale=1.0, **kwargs) returns (a + b + sum(args)) * scale. What is demo(1, 2, 3, 4, scale=0.5)[“total”]?

  • 10.0
  • 5.0
  • 11.0
  • 2.5

Three calls, one function

{'total': 11.0, 'options': {}}, {'total': 5.0, 'options': {}}, {'total': 22.0, 'options': {'theme': 'dark'}}. scale sits after *args, so it can only be passed by name — the pattern behind df.sort_values("x", ascending=False).

lambda: a function with no name

[1, 4, 9, 16] twice, then ['LA', 'NYC', 'Chicago']. The key= argument is where lambdas live: sorted, max, min, and pandas’ .apply(lambda r: …). A lambda holds one expression — if you need two lines, write a def.

Docstrings and help

The triple-quoted string right under def is the docstring; help() (or greet.__doc__) shows it. help(pd.read_csv) is how you read the 50 keyword arguments without leaving the notebook.

Scope: what happens to x inside?

x = “global”; def demo(): x = “local”; return x. What do print(demo()) and print(x) show?

  • local then global
  • local then local
  • global then global
  • An UnboundLocalError

Your turn: clean_and_count(words)

The notebook’s mini-exercise. Write clean_and_count(words): lower-case each word, strip the punctuation .,!? from its ends (w.strip(".,!?")), and return a dict word → count. Use counts.get(w, 0) + 1.

f-strings: numbers into text

pi = 3.1415926. What does f"{pi:8.3f}" give — including the spaces?

print(repr(f"{pi:8.3f}"))

' 3.142'

Maya scored 93.5, 3.14, ' 3.142', 3. Two more you will want for a finance memo: :.2% renders a return as a percentage, :, puts thousands separators in a dollar amount.

zip: walk two sequences together

list(zip([“Ana”, “Bo”, “Cy”], [88, 92])) returns?

  • [("Ana", 88), ("Bo", 92), ("Cy", None)]
  • [("Ana", 88), ("Bo", 92)]
  • A ValueError
  • [("Ana", "Bo", "Cy"), (88, 92)]

try / except: turn a crash into a value

5.0 then inf. float("inf") is the IEEE infinity — bigger than any number, so min() over results still works. Catch the specific exception; a bare except: also swallows typos and KeyboardInterrupt.

Which exception is it?

Which exception does int(“abc”) raise?

  • KeyError
  • TypeError
  • IndexError
  • ValueError
Exception Raised by Seen in this chapter
ZeroDivisionError x / 0 safe_divide
KeyError d["missing"] student["gpa"] before it was added
IndexError lst[99] slicing past the end
TypeError t[0] = 1, "a" + 1, set()[0] tuples, sets
ValueError int("abc") converting text
NameError using an undefined name misspelt variable

Mini-Project: Tiny Text Stats

Everything in one place: a string, split, a loop, a dict, .get, a lambda sort, and an f-string. Which word appears most in the paragraph?

Your turn: the top three words

Build top3 — the three most frequent words as (word, count) pairs, most frequent first. Clean each word (lower-case, strip .,!?) before counting; the starter forgets to, so Data and data. are counted separately.

Target output: data 4, insight 3, good 2. The same skeleton — clean, count, sort — is a bag-of-words sentiment score on an earnings-call transcript.

What you discovered

  • A function without return returns None; defaults fill missing arguments, but a mutable default is shared across calls.
  • *args arrives as a tuple, **kwargs as a dict; keyword-only parameters sit after *args.
  • lambda is a one-expression function; its home is key= in sorted, max, .apply.
  • Assignment inside a function is local; the global is untouched.
  • f"{x:8.3f}", :.2%, :, format numbers; zip stops at the shortest input.
  • try/except SpecificError converts a crash into a value you can reason about.

Next: Chapter 1 — the pandas Series: a dictionary that learned arithmetic.

Mistakes Library: Reinhart–Rogoff (2010–2013)

Warning

In Growth in a Time of Debt (2010), Carmen Reinhart and Kenneth Rogoff reported that countries with public debt above 90 % of GDP grew at −0.1 % a year on average — a number cited by the UK Treasury, the European Commission and the US Congress to justify austerity after 2010.

In April 2013 Thomas Herndon, a UMass Amherst graduate student, obtained the working spreadsheet for a replication assignment. The averaging formula covered rows 30–44 instead of 30–49: Australia, Austria, Belgium, Canada and Denmark were silently excluded. Combined with a selective country-year sample and an unusual weighting, the corrected average for the >90 % bucket was +2.2 %, not −0.1 % (Herndon, Ash and Pollin, Cambridge Journal of Economics, 2014).

Lesson for this chapter: a range that stops one row early is a slicing error — rows[30:45] when you meant rows[30:50]. Print len() of every slice before you average it, and write the calculation as a function you can test on a five-row example.

Decision Memo — Should the desk automate transcript keyword counts?

The mini-project is a prototype. The deliverable is a recommendation.

To: Head of Equity Research From: <Your name>, research associate Subject: Replace manual keyword tallies on earnings-call transcripts with a Python script Date: 2026-09-15

Recommendation: Adopt the clean_and_count pipeline for all S&P 500 calls this quarter; keep one analyst on spot-checks.

Evidence: - On the 12-transcript pilot the script reproduced the analysts’ counts for 11 of 12 target words; the one miss ("guidance." vs "guidance") was a punctuation-stripping rule, now fixed. - Run time: 0.4 s per transcript vs ~25 minutes by hand — 500 calls per quarter is ~200 analyst-hours saved.

Caveats: - Case-folding merges "Apple" (the firm) with "apple" (the fruit) — acceptable for finance vocabulary, wrong for names. - Counting words ignores negation (“not confident”); this is a count, not a sentiment score.

Next step: Wrap the counts in a pandas DataFrame (Chapter 2) and test whether the word rate predicts next-day returns (Chapter 4).

Working with an AI Copilot

An LLM writes fluent Python. It also writes plausible Python that fails on exactly the traps in this chapter.

  1. Ask for the trap explicitly. “Write add_city(c, cities=[])” will be accepted as-is. Instead: “Write it and tell me whether any default argument is mutable, and why that matters.”
  2. Demand a tiny test. Paste the FizzBuzz or clean_and_count starter and say: “Give me three input/expected-output pairs I can run before trusting this.” If the copilot cannot produce the expected output for ["Data", "data."], it does not understand the cleaning rule either.
  3. Never let it silence errors. A copilot’s favourite fix is try: … except: pass. Ask “which specific exception are you catching, and what value do you return instead?” — if the answer is “all of them”, reject the patch.

Chapter Summary

Concept Tool
Arithmetic and division + - * / // % **; / → float, // floors
Types and conversion type(), int(), float(), str(), bool()
Sequences s[start:stop:step], len(), in
Strings .strip(), .upper(), .replace(), .split(), immutable
Lists / tuples .append(), .insert(), .pop(), .copy(); (a, b) immutable
Sets / dicts set(), .add(), .union(); d[k], d.get(k, default), .items()
Control flow if/elif/else, for, enumerate, range, while, break, continue
Comprehensions [f(x) for x in xs if c], {k: v for …}
Functions def, defaults, *args, **kwargs, lambda, docstrings, scope
Text and errors f"{x:.2f}", zip, try/except SpecificError

Next: Chapter 1 — Data Structures and Methods of Series.

Discussion Questions

  1. b = a for a list and b = a for an integer behave differently when you then “change b”. Explain, using the words object, name, and immutable, why there is in fact no difference in what = does.
  2. A colleague computes the average of a 20-price window with prices[i:i+19]. What is wrong, how would you catch it with one print, and what does the Reinhart–Rogoff episode add to the argument?
  3. When would you choose d[key] over d.get(key, default) in production code, given that only one of them can crash your pipeline at 3 a.m.?
  4. Rewrite the Tiny Text Stats loop as a single comprehension plus collections.Counter. What did you gain, and what did the beginner reading your code lose?