The Analyst's Path

Phase 12Finance Plus, AI and the quant-code trackFinance trackCS1.0113,489 words1 h 15 min of readingabout 22 hours of studyfree

Programming from Zero: Python I

Every analyst already runs programs. A spreadsheet formula is a tiny program; a set of filter-and-sort clicks is a program you re-perform by hand every single time, forgetting a step here and there, and calling the result "the model." The difference between…

Branch: CS Foundations (quant-cs) · Finance galaxy, Quant-Code region · Node CS1.01 · ~22 focused hours · Gate: quiz ≥ 85% + code-pack ≥ 85% (hidden tests green on at least 85% of exercises)

Every analyst already runs programs. A spreadsheet formula is a tiny program; a set of filter-and-sort clicks is a program you re-perform by hand every single time, forgetting a step here and there, and calling the result "the model." The difference between that and real programming is not sophistication, it is that a written program is an instruction you can re-run, inspect, test, and trust, instead of a sequence of clicks you half-remember. This node is where that shift begins, from absolute zero. No prior coding is assumed. No calculus, no computer-science background, nothing except ordinary arithmetic and the willingness to be exact about things you have always been vague about, because a computer, unlike a spreadsheet or a colleague, will not guess what you meant.

Advertisement

You are about to learn Python, which today is the working language of data analysis, quantitative finance, machine learning, and most of the software that runs the modern financial industry. You are going to learn it the honest way: not by memorizing syntax, but by building (line by line, mistake by mistake) a correct mental model of what a computer actually does when it runs your code. That mental model is the actual skill. Syntax you can look up in ten seconds; a broken mental model produces bugs you can stare at for an hour without seeing.

Everything in this module points at one running example, because programming-from-zero taught in the abstract evaporates by the next morning: a bundled snapshot of daily closing prices, one from India's NSE and one from the US's NYSE, and the single question "how much did this go up or down, and by how much?" By the end you will write, run, and debug small Python programs that answer that question correctly, robustly, and reusably, first by hand-tracing what the computer does, then by actually running real Python. Every idea in this module also becomes a graded, auto-checked coding exercise in this module's code pack (pack id CS1.01, exercises CS1.01-e01 through CS1.01-e08), real Python, executed live in your browser, checked against hidden tests you cannot see and cannot hardcode your way past. Nothing here is decorative. If you can explain it, you can code it; if you can code it, the pack will prove it.


Learning objectives

By the end you can:

  1. Explain, in plain language, what a program is and what the Python interpreter actually does with your code (read it, translate it, and execute it one instruction at a time) and distinguish a statement (an instruction) from an expression (something that produces a value).
  2. Name Python's core built-in types (int, float, str, bool, and NoneType) recognize a literal of each, and predict what type(x) returns for any value you can construct.
  3. Explain what a variable actually is in Python (a name bound to a value, not a labeled box that contains it), predict the effect of assignment and re-assignment, and trace multiple-assignment and swap idioms correctly.
  4. Evaluate arithmetic, comparison, and logical expressions by hand, including operator precedence, integer vs. true division (// vs /), the modulo operator (%), and augmented assignment (+= and friends).
  5. Build and reason about strings: concatenation, f-strings with format specifiers (:.2f and friends), and the handful of string methods (.strip(), .split(), .upper()/.lower()) needed to clean and format real data.
  6. Use a Python list as an ordered sequence of values well enough to index it (including negative indices), measure it with len(), grow it with .append(), and iterate over it, while knowing this is a first taste, not the full picture (CS1.02 owns the deep list/tuple/dict/set comparison).
  7. Write and trace control flow (if/elif/else for decisions, while for open-ended repetition, for for repetition over a known sequence) and correctly predict the output of nested and combined control-flow code.
  8. Write functions: parameters, a return value, the difference between return and print, and the basic scoping rule that a name assigned inside a function is local to it, and explain why breaking a program into small, well-named functions is not decoration but the single highest-leverage habit in this entire module.
  9. Perform basic input/output: format and print numbers precisely with f-strings, read a value with input(), and (critically) remember that everything read from input() or a text file arrives as a str and must be deliberately converted before it can be used as a number.
  10. Apply everything above to the module's running problem: given a list of daily closing prices from an Indian (₹, NSE-style) or US ($, NYSE-style) snapshot, compute the price change, the total return, the count of up-days, the average daily change, and the best/worst single day, first by hand, then in real, running, tested Python (this module's code pack, CS1.01).
  11. (Productivity objective: R10 duality.) Use an AI coding assistant as a copilot for exactly the parts of this module you already understand by hand, scaffolding, boilerplate, explaining an unfamiliar error message, while never letting it write code whose logic you cannot personally verify, and while always running the tests yourself before trusting the result.

The duality, stated once (R10). Objectives 1–10 are the understanding objective: the gate (quiz ≥ 85%, code-pack ≥ 85% with hidden tests green) rewards being able to trace, write, and debug this code yourself, by hand and in the editor. Objective 11 is the productivity objective: knowing how a real developer uses an AI copilot on top of that understanding, never instead of it. A copilot can type faster than you. It cannot pass this module's hidden tests for you, because passing them requires code that is actually correct on inputs the copilot, and you: have never seen.


Prerequisites & connections

Builds on. Nothing. This is the first node of the quant-cs branch and, per the program's day-one entry rule, it unlocks from the very start, no finance-phase progress and no other branch is required. The only assumptions are comfort with ordinary arithmetic (addition, subtraction, multiplication, division, percentages) and the patience to be pedantic, because a computer executes exactly what you wrote, not what you meant. If you have never written a line of code before, you are exactly the intended reader.

Feeds forward. Directly into CS1.02 (Python II, data structures, comprehensions, mutability, pure functions, and idiomatic classes), which is where the "first taste" of lists in this module grows into a real understanding of when to reach for a list vs. a tuple vs. a dict vs. a set. CS1.03 builds on this module's variable/type model to explain why Python behaves the way it does at the machine level (stack, heap, and a contrasting compiled language). CS1.04 (recursion and Big-O) assumes you can already write and trace loops and functions fluently, that fluency is built here. Further out, the DA (data science) and QD (quant developer) branches both recommend CS1.02 before entry (a soft, non-blocking recommendation, not a gate (R11)) but every line of pandas, NumPy, or backtesting code you will eventually write is built out of exactly the primitives taught in this module: values, variables, control flow, functions, and I/O. There is no shortcut around this node; there is only this node, done properly.

Cross-branch companions (soft, not gates). None required for CS1.01 itself. If you are approaching this module already comfortable with basic finance vocabulary (return, price, growth) from the analyst-track phases, the running example will feel familiar; if you are not, the module explains every finance term it uses from scratch, because R11 guarantees you can start here with zero prior coursework.


Core teaching content

1. What a program actually is

A program is a sequence of instructions, written in a language a computer can execute, that runs from top to bottom (with detours, which you will learn to control) and does exactly what it says, never what you meant, only what you wrote. Python is one such language. When you run a Python program, a piece of software called the interpreter reads your text, translates each instruction into something the machine can actually execute, and carries it out immediately, one instruction at a time, before moving to the next. This matters because it gives you your first debugging superpower: a Python program is not a mysterious block that either "works" or "doesn't", it is a strict sequence of individual steps, each of which either succeeded or is the one that failed. When something goes wrong, your job is never "guess what's broken in general"; it is "find the one line where reality first diverged from what you expected," which you do by reading the error message and by mentally, or literally, running the program one line at a time.

Python code is built from two kinds of things, and telling them apart will save you real confusion later. A statement is a complete instruction ("do this") such as an assignment (x = 5) or an if block. An expression is anything that produces a value when evaluated, 2 + 2, len(prices), total_return(prices) are all expressions, because each one, when the interpreter evaluates it, becomes a single value (4, some integer, some float). Every expression can appear anywhere a value is expected; a bare statement cannot. This distinction is why print(x = 5) is not the same as print(x); x = 5, assignment is a statement, not an expression that hands back a value in the ordinary sense, and Python will simply refuse a plain x = 5 where it expects an expression. You do not need to memorize this rule in the abstract; you need to recognize it the first time a "why won't this go here?" error appears, which the Common Mistakes section revisits directly.

You will meet Python two ways in this course: as a REPL (Read–Evaluate–Print Loop, a live, one-line-at-a-time prompt where you type an expression and immediately see its value, ideal for trying small things) and as a script, a saved sequence of statements run start to finish. This module's code pack runs your code the second way: your submission is a small script (usually just a function definition), executed once, then tested.

2. Values and their types: int, float, str, bool, None

Every piece of data in Python is a value, and every value has a type that determines what you can do with it. Five types carry almost this entire module:

  • int: a whole number, positive, negative, or zero, with no decimal point: 5, -12, 0, 1_000_000 (underscores are allowed as pure visual separators in literals and are ignored by the interpreter).
  • float: a number with a decimal point (or written in scientific notation): 3552.00, -0.05, 1.5e3 (which means 1.5 × 10³ = 1500.0). Almost every price, return, and percentage in this module is a float.
  • str (string) (text, always wrapped in quotes, single or double (Python treats them identically) pick one style and be consistent): "NSE_TCS", 'a single quote works too', "165.00", note that last one is a string that looks like a number, which is a completely different thing from the number 165.00, and confusing the two is the single most common beginner bug in this module (Common Mistakes covers it in depth).
  • bool (exactly two values, True and False (capitalized, no quotes) they are not strings), the result of every comparison (3 > 2 evaluates to True) and the currency of every if statement.
  • None: a single special value meaning "no value here," Python's version of a deliberate blank. A function that falls off its end without a return statement hands back None, which is a real, checkable value (x is None), not an error and not zero.

You can always ask Python what type a value has with the built-in type() function: type(3552.00) returns <class 'float'>; type("165.00") returns <class 'str'>. Get comfortable typing type(x) on anything you are unsure about, it is free, instant, and the single fastest way to resolve "wait, what is this thing?" while debugging.

Two conversions you will use constantly: float("165.00") turns the string into the number 165.0; str(165.0) turns the number back into the string "165.0". These are not automatic. Python will not silently decide that the string "165.00" "means" the number 165.0, you must ask for the conversion explicitly, every time, with float(...), int(...), or str(...). This is called Python being dynamically typed but strongly typed: a variable can hold any type and can be reassigned to a different type later (dynamic), but Python will never silently convert one type into another behind your back (strong, contrast this with languages that will happily and silently turn "5" + 3 into 8 or into "53" depending on the language; Python does neither, it raises an error, forcing you to say what you meant).

3. Variables: names bound to values, not boxes containing them

A variable in Python is a name that refers to a value, think of it as a label you stick onto a value, not a labeled box you pour the value into. This distinction sounds pedantic until the first time it saves you from a bug (and CS1.02 will make it load-bearing when you meet mutable collections). For now, the practical consequences are:

  • Assignment (price = 3552.00) creates the name price and binds it to the value 3552.00. If the name already existed, the old binding is simply replaced, nothing is "overwritten in place" the way it would be in a box metaphor; the name is just re-pointed at a new value.
  • Re-assignment is completely ordinary: price = 3552.00 followed later by price = 3560.00 is not an error, and the earlier value 3552.00 is not "changed", it still exists as a value; the name price now refers to a different value. If nothing else refers to the old value any more, Python quietly reclaims the memory (garbage collection), not something you manage by hand.
  • Multiple assignment lets you bind several names in one line: open_price, close_price = 3500.00, 3552.00 binds open_price to 3500.00 and close_price to 3552.00 in one statement, matched by position. This is also how you swap two variables without a temporary third one: a, b = b, a, the right-hand side is fully evaluated (both old values) before either name on the left is rebound, so this genuinely swaps them, which surprises people coming from languages where a manual temporary variable is required.
  • Naming rules and convention. A Python name must start with a letter or underscore, contain only letters/digits/underscores, and cannot be one of Python's reserved keywords (if, for, return, and so on). Convention (not a rule the interpreter enforces, but one every professional Python codebase follows, per PEP 8) is snake_case for variables and functions: total_return, not TotalReturn or totalReturn. Follow it from day one; code review in the professional world treats it as a baseline, not a nicety.

Names are also how you avoid the single worst habit a beginner can form: repeating a literal number in multiple places. If you write 3552.00 / 3500.00 - 1 in three different spots in a program and the data changes, you now have three places to remember to update, and you will forget one. Bind it to a name once (total_return = close_price / open_price - 1) and change the inputs, never the formula, when the data changes. This single habit, more than any syntax rule, is what separates code that survives contact with real, changing data from code that quietly goes stale.

4. Expressions and operators

Python's arithmetic operators are +, -, *, / (division, always produces a float, even 4 / 2 gives 2.0, not 2), // (floor division, divides and rounds down to the nearest whole number: 7 // 2 is 3, and -7 // 2 is -4, not -3, because floor division always rounds toward negative infinity, not toward zero), % (modulo, the remainder after floor division: 7 % 2 is 1), and ** (exponentiation: 2 ** 10 is 1024). The / vs. // distinction is the one every learner coming from another language trips on at least once: Python 3's / is true division and always returns a float, even when the inputs are whole numbers and the result "looks like" it should be an int; if you specifically want the floor of a division as a whole-number-typed result, you ask for // explicitly.

Operator precedence follows the ordinary mathematical convention (** binds tightest, then *, /, //, %, then +, -, evaluated left to right within a tier), and parentheses always win, when in doubt, add parentheses; they cost nothing and remove all ambiguity for the next person reading your code, including future-you. prices[-1] / prices[0] - 1, this module's central formula, reads as (prices[-1] / prices[0]) - 1 because division binds tighter than subtraction; write the parentheses anyway the first several times you use a new formula, until the shape is second nature.

Comparison operators: == (equal), != (not equal), <, >, <=, >=, each produce a bool. The single most consequential beginner typo in this entire language is writing x = 5 (assignment) when you meant x == 5 (comparison), they look one character apart and do entirely different things; Common Mistakes drills this explicitly. Logical operators (and, or, not) combine booleans: pct_change > 0 and pct_change < 0.10 is True only when both sides are True. Python also lets you chain comparisons directly, which reads more like ordinary math and avoids the and entirely: 0 < pct_change < 0.10 means exactly the same thing.

Augmented assignment: total += daily, count += 1, price *= 1.05, is shorthand for "take the current value of the name, apply the operator, and rebind the name to the result": total += daily means exactly total = total + daily. This shorthand is not cosmetic; it is the standard idiom for accumulating a running total or a running count inside a loop, and you will write it dozens of times in this module's exercises (count_up_days, average_daily_change both use it).

5. Strings in practice: building and formatting text

A string is text between quotes. You can join (concatenate) strings with + ("NSE" + "_" + "TCS" gives "NSE_TCS"), but concatenation only works string-to-string: "Return: " + 0.05 raises a TypeError, because Python refuses to silently guess whether you meant to convert the number to text or do something else. The fix is always an explicit conversion: "Return: " + str(0.05).

Far more useful, and what you will use for essentially all formatted output in this module, is the f-string: prefix a string literal with f and put any expression inside curly braces, and Python evaluates it and drops the result straight into the text. f"{label}: return={pct:.2f}%" evaluates label and pct, converts them to text automatically (no str() needed inside an f-string), and (critically) the :.2f after pct is a format specifier meaning "format this as a fixed-point decimal number with exactly 2 digits after the decimal point," which is exactly how you turn a computed float like 10.000000000000009 (the kind of tiny floating-point noise you will see constantly, see Common Mistakes) into the clean, presentable 10.00 a real report should show. You will use :.2f in essentially every piece of formatted output this module asks you to produce, including the code pack's format_report exercise.

Three string methods you need now (Python objects carry methods, functions attached to a value, called with a dot, value.method()): .strip() removes leading and trailing whitespace (spaces, tabs, and the newline character \n) from a string, indispensable for cleaning up a raw line of text read from input() or a file, which almost always carries an invisible trailing \n you did not ask for and will not see just by looking at a printed value. .split(sep) breaks a string into a list of pieces wherever sep occurs ("3552.00,3560.00".split(",") gives ["3552.00", "3560.00"]), you will meet this properly once lists are introduced below, and again in depth in CS1.02 and CS2.03. .upper() / .lower() normalize case, useful for comparing text without worrying about capitalization ("NSE".lower() == user_input.lower()).

6. A first collection: the list

You cannot compute a return "over a price series" with only single numbers, you need an ordered collection of them. Python's most common such collection is the list: an ordered, changeable sequence of values, written with square brackets and commas: prices = [3500.00, 3552.00, 3518.00, 3560.00]. This module teaches only what you need to run the return/growth calculations correctly; the full reasoning about when to reach for a list versus a tuple, dict, or set, and the deeper operations (comprehensions, slicing rules, mutability and aliasing): is CS1.02's job, and this module deliberately does not re-teach it (R1). What you need now:

  • Indexing. prices[0] is the first element (Python counts from zero, not one, this is the single most consistent source of off-by-one bugs for every beginner, in every language that indexes from zero, and there is no way around simply drilling it until it is automatic). prices[-1] is the last element, negative indices count from the end, so -1 is "last," -2 is "second-to-last," and so on; this is not a trick, it is the idiomatic, professional way to say "the last element" in Python, and prices[-1] / prices[0] - 1 (this module's central formula) reads naturally once this clicks: "last price over first price, minus one."
  • Length. len(prices) returns how many elements the list holds. Never hardcode a list's length as a literal number in your code, always ask len(...), because the whole point of a function is that it should work correctly on a list of 4 prices and a list of 400, without you touching the code.
  • Iteration. for price in prices: visits each element in order, binding price to each one in turn, this is how you process every element without knowing in advance how many there are (see Control flow, below).
  • Growing a list. prices.append(3675.00) adds one new element onto the end. Note this is a method call that changes the list itself (prices now has one more element) and returns None, it does not return a new list, which is a common early confusion (prices = prices.append(x) is a bug: it sets prices to None).

You will see and use lists constantly from here on; you will not, in this module, need dictionaries, sets, or tuples in earnest, those arrive properly in CS1.02.

7. Making decisions: if / elif / else

An if statement runs a block of code only when a condition is True:

if pct_change > 0.0005:
    result = "gain"
elif pct_change < -0.0005:
    result = "loss"
else:
    result = "flat"

Read this exactly as written: Python evaluates pct_change > 0.0005; if it is True, it runs the indented block under if and skips every elif/else below it entirely, the chain stops at the first True condition it finds, top to bottom. If the first condition is False, it tries the next elif condition, and so on; if none of them are True, it falls through to else, which always runs when nothing above it matched (an else has no condition of its own, it is "otherwise"). You can have any number of elif branches, and both elif and else are optional, a bare if with nothing else is completely valid.

Indentation is not decoration in Python, it is the syntax. Where other languages use braces { } to mark a block, Python uses consistent indentation (4 spaces is the near-universal convention; do not mix tabs and spaces. Python 3 will refuse to run code that does). Every statement indented at the same level under an if/elif/else/for/while/def is inside that block; the moment the indentation returns to the outer level, you are back outside it. This is why a stray extra space, or a missing one, produces either an IndentationError or, far worse, code that runs without complaint but does the wrong thing, because a line you thought was "inside" the if was actually one level out.

Every condition in Python is ultimately a bool, but Python also treats several other values as falsy in a boolean context even though they are not literally False: the number 0, the empty string "", and (once you reach CS1.02) an empty list [] all count as False when used directly as a condition; every other value is truthy. if prices: is genuinely idiomatic Python for "if the list is not empty", you will see it in real code, though this module's exercises stick to explicit comparisons for clarity while you are still building the underlying habit.

8. Repetition: while and for

A while loop repeats its block for as long as a condition stays True, re-checking the condition before every pass:

balance = 1000.0
years = 0
while balance < 2000.0:
    balance *= 1.07
    years += 1

This is the right tool when you do not know in advance how many repetitions you need, here, "how many years of 7% growth until the balance doubles" is exactly the kind of question a while loop answers naturally, and you will trace one by hand in the Worked examples. The single most dangerous mistake with while is forgetting to change something the condition depends on inside the loop body, if years were never incremented and balance never grown, this loop would run forever (an infinite loop), which is why every code-pack exercise in this program runs under a wall-clock timeout that will simply kill a submission that never finishes, rather than hang the browser tab.

A for loop repeats its block once for each element of a sequence, a list, or the output of range(...), and is the right tool the moment you know what you are iterating over, which is the overwhelming majority of cases in this module:

total_up = 0
for i in range(1, len(prices)):
    if prices[i] > prices[i - 1]:
        total_up += 1

range(1, len(prices)) produces the whole-number sequence 1, 2, 3, ..., len(prices) - 1 (the stop value is never included, range(a, b) never yields b itself, another near-universal source of off-by-one confusion until it is drilled). Here it deliberately starts at 1, not 0, because comparing prices[i] to prices[i - 1] at i = 0 would reach for prices[-1], the last element, not "one before the first," which is not an error Python will catch for you; it will silently hand you the wrong comparison. This exact loop shape (index from 1 to len(prices), compare prices[i] to prices[i - 1]) is the backbone of three of this module's eight code-pack exercises (count_up_days, average_daily_change, best_and_worst_day); learn this one shape precisely and you have effectively learned three exercises.

break exits a loop immediately, and continue skips the rest of the current pass and moves to the next one; both are used sparingly in this module (a well-structured if/for combination usually reads more clearly than a break buried inside one), but you should recognize them when you see them.

9. Functions: naming a piece of logic so you never repeat it

A function is a named, reusable block of code that takes zero or more parameters as input and (optionally) returns a value as output:

def total_return(prices):
    return prices[-1] / prices[0] - 1

def begins the definition; total_return is the name; prices (inside the parentheses) is the parameter, a name that, inside this function only, refers to whatever value is passed in when the function is called. total_return([100, 110]) calls the function with the argument [100, 110]; inside the function, prices is bound to that list, the body runs, and return hands the computed value back out to wherever the call happened, ending the function's execution immediately (any code after a return inside the same branch never runs).

Get the difference between return and print exactly right, because conflating them is one of the most common beginner errors and the single fastest way to fail a graded exercise you actually solved correctly in every way except this one: print(x) displays x to the screen for a human to read and gives the function itself nothing to hand back to whatever called it; return x hands the value x back to the calling code so it can be used in further computation (stored in a variable, passed to another function, compared, and so on), and displays nothing at all. A function that prints its answer instead of returning it will look correct when you eyeball its output, and will fail every hidden test that calls it and inspects what came back, because what came back is None (see §2). Every function in this module's code pack must return its answer; several worked examples deliberately build a string that a real program would go on to print, precisely to keep this distinction sharp.

Parameters can have default values: def classify_move(pct_change, threshold=0.0005): lets a caller either supply a threshold or omit it and get 0.0005 automatically, a small convenience this module's exercises do not require but that you will see constantly in real code from here on. Functions can also take zero arguments, or multiple, matched to parameters by position (price_change(old_price, new_price), the order you pass arguments in matters unless you name them explicitly with price_change(new_price=105, old_price=100), which Python also allows).

Scope is the last piece: a name assigned inside a function is local to that function and disappears the moment the function returns, it cannot leak out and overwrite a same-named variable outside. count inside count_up_days has nothing to do with any other count you might have lying around in your own testing code; each function call gets its own fresh set of local names. (The reverse, a function reading a name defined outside it, is possible but is a habit this module deliberately avoids: every function you write here should compute its answer purely from its parameters, taking nothing from, and changing nothing in, the wider program. This property, the same inputs always produce the same output, and nothing outside the function is touched, is called a pure function, and CS1.02 builds directly on it; every one of this module's eight code-pack exercises is written this way on purpose, as a habit worth having from your very first function.)

Why does this matter more than any single syntax rule in this section? Because breaking a program into small, well-named, single-purpose functions is the mechanism by which real programs stay understandable as they grow past the ten or twenty lines you can hold in your head at once. A 200-line program with no functions is one giant, tangled thing you must re-read in full to change anything; a 200-line program built from ten well-named 20-line functions is ten small, separately understandable, separately testable pieces. You are building that habit starting with your very first def.

{
  "skill": "Scaffolding a function you already understand, and reading an unfamiliar error message",
  "use": "Once you can already state, on paper, exactly what a function should take in and return — its parameters, its logic in your own words, and the return value — use an AI coding assistant to generate the boilerplate `def` and a first-draft body, and to translate an error message (a traceback, an IndentationError) into plain English faster than searching for it yourself.",
  "tools": ["AI coding assistants — Claude, ChatGPT, GitHub Copilot, Cursor", "Your editor's built-in error/traceback display"],
  "prompt": "I'm learning Python. Here is what my function `<NAME>` should do, in my own words: <YOUR PLAIN-ENGLISH SPEC>. Write the `def` signature and a first draft, using only built-in Python (no imports). Then explain, line by line, why each line is there — I want to understand it, not just copy it. Separately: here is an error message I got: <PASTE TRACEBACK>. Explain in plain English what it means and which line is the actual cause.",
  "verify": "Read every line of the generated code against your own paper spec before you paste anything into the editor. Then RUN this module's code pack's visible tests yourself — an AI-generated function can look plausible and still be subtly wrong (an off-by-one in a range, a `print` where a `return` belongs) and will not tell you so; only the tests will. Never submit AI-written code you cannot personally explain, line by line, to another person.",
  "diy": "You must already be able to state the function's inputs, logic, and output in your own words BEFORE asking a copilot to draft it — that is the whole first half of this section (def, parameters, return). If you cannot describe what the function should do without the tool, you are not ready to accelerate yet; write it slowly by hand first.",
  "market": "IN"
}

10. Input/Output: getting data in, and getting answers out precisely

Output in this module means print(), almost always with an f-string carrying a format specifier so the number you show a human is clean, not raw floating-point noise. print(f"Return: {pct:.2f}%") is the idiomatic pattern you will use constantly. print() can take multiple comma-separated arguments, which it joins with a single space and a trailing newline by default, print("start=", 100, "end=", 110) prints start= 100 end= 110, with a space you likely did not want before the numbers; an f-string gives you exact control over every character, which is why this module's format_report exercise insists on it.

Input means input(), it displays an optional prompt, waits for the user to type a line, and returns exactly what was typed, as a str, always, even if the user typed 165.50, input() hands you back the four-character text "165.50", not the number 165.5. This single fact is the most consequential I/O rule in the module: input(), and reading a line out of a text file, never gives you a number. It always gives you a string that might look like a number, and you must convert it explicitly with float(...) or int(...) before you can do arithmetic with it. Skipping this step does not always crash immediately ("3" + "3" is perfectly legal Python and evaluates to "33" (string concatenation, not addition)) which is exactly what makes this bug insidious: it can silently produce a plausible-looking wrong answer instead of an honest crash. You will meet this failure mode directly in Worked example 2 and again in Common Mistakes, and this module's parse_price exercise exists specifically to drill the fix: float(raw.strip()), stripping any stray whitespace or newline before conversion, because a line read from a real file almost always carries an invisible trailing \n.

A brief, honest note on real files: opening and reading an actual file from disk (with open("prices.txt") as f: ...) is a CS2.01 topic, not this one. Pyodide's in-browser Python has no access to your computer's real filesystem, and the deeper command-line/environment/project-layout context that makes file-handling meaningful belongs to that later module. What this module does teach, because it is squarely an I/O skill and squarely needed for the running example, is what to do with a line of text once you have it in hand, whichever way it arrived: input(), one line of a bundled dataset, or a string literal standing in for either. parse_price and the .strip()/float() pattern behind it are exactly that skill, decoupled from how the raw text arrived.

11. Putting it together: return and growth on a real price series

Here is the module's entire payoff, assembled from every piece above. Suppose you are handed this module's bundled snapshot (a small, explicitly synthetic (seeded, illustrative) not a real historical quote; see the note at the top of Worked examples) run of seven daily closes for an NSE-style Indian listing, in ₹, and seven for an NYSE-style US listing, in $:

nse_prices = [3500.00, 3552.00, 3518.00, 3560.00, 3600.00, 3585.00, 3675.00]  # ₹, synthetic, seed 42
us_prices  = [150.00, 151.50, 148.00, 152.00, 155.00, 154.00, 165.00]         # $,  synthetic, seed 7

Every question an analyst actually asks about a price series is now just a function away, and (because you wrote each one as a pure function of prices) the exact same code answers the question for either market, or for a fresh series next week, with zero changes:

  • "How much did it move from open to close?" → price_change(old, new) → a single subtraction.
  • "Is today a gain, a loss, or flat?" → classify_move(pct_change) → an if/elif/else.
  • "What was the total return over the whole run?" → total_return(prices) → prices[-1] / prices[0] - 1, this module's single most important formula.
  • "How many days closed higher than the day before?" → count_up_days(prices) → a for loop with a running count.
  • "What's the average day-to-day move?" → average_daily_change(prices) → a for loop with a running total, divided by the count of comparisons, and, as you will see in Worked example 5 and code-pack exercise CS1.01-e05, a genuinely different number from total return, for a reason worth understanding precisely, not just accepting.
  • "What was the best single day, and the worst?" → best_and_worst_day(prices) → a for loop building a list of daily changes, then max(...) and min(...).
  • "Give me a clean one-line report." → format_report(label, prices) → an f-string with :.2f formatting, ready to print().
  • "I only have this as raw text, a line from a file, or from input()." → parse_price(raw) → float(raw.strip()), every time, without exception.

Every one of these eight functions is a real, individually graded exercise in this module's code pack (CS1.01, exercises CS1.01-e01 through CS1.01-e08), each one checked against tests you can see (to learn the expected shape) and hidden tests you cannot (built from both the ₹ series and the $ series above, so a function that only works on the specific numbers you happened to test by hand will be caught immediately). Open the pack, write each function exactly as you reasoned it out above, run it, and watch real Python (not a simulation, not a trace on paper) confirm you got it right.

{
  "skill": "Turning a working-by-hand calculation into a first draft of real code, for a task you already fully understand",
  "use": "After you have hand-traced a calculation (e.g. average_daily_change over a short price list, on paper, arriving at the correct number yourself), use an AI assistant to draft the Python function — then treat its draft exactly the way you would treat a junior colleague's first pass: useful, fast, and not yet trusted.",
  "tools": ["AI coding assistants — Claude, ChatGPT, GitHub Copilot", "This module's code pack's own visible tests, as your first line of defense"],
  "prompt": "I already worked this out by hand: <YOUR HAND TRACE, e.g. 'average_daily_change([100,110,99]) should be 0.0, because the daily changes are +0.10 and exactly -0.10'>. Write a Python function `<NAME>(prices)` that computes this generally, for any list of prices, using only a for-loop and built-ins (no imports). Do not just match my one example — show me it is general.",
  "verify": "Run the function on the SAME example you hand-traced first — if it does not reproduce your own by-hand answer, the draft is wrong, full stop, regardless of how confident or clean the code looks. Then run it against this module's visible AND hidden tests (CS1.01 code pack) before trusting it on anything else; a subtly wrong loop bound (off-by-one) often still 'looks right' on casual reading.",
  "diy": "You must be able to hand-trace the calculation on a short example YOURSELF, correctly, before asking a tool to generalize it — that is exactly what Worked examples 4 and 5 and Practice set items P7-P10 build. An AI cannot generalize a calculation you cannot yet do by hand.",
  "market": "US"
}

Common mistakes & how experts think differently

  1. Writing = when you mean ==. if pct_change = 0.05: is not "did it equal 5%", it is a syntax error in Python (unlike some languages, Python will not even let a bare assignment sit inside an if condition, so this particular slip is at least caught immediately with a clear message), but the version that does silently pass, x = 5 where you meant x == 5 as a standalone check, produces a program that quietly does the wrong thing with no error at all. The expert habit: read = out loud as "is assigned" and == as "is equal to," every single time, until the two are never confused again.
  2. Assuming a number printed cleanly means it is clean. 3675.00 / 3500.00 - 1 does not evaluate to a perfectly clean 0.05 in Python, it evaluates to 0.050000000000000044, tiny floating-point representation noise that is completely normal, present in every language that uses standard binary floating-point arithmetic, and not a bug in your code. The classic demonstration: type 0.1 + 0.2 into any Python prompt and you get 0.30000000000000004, not 0.3. The expert habit, drilled throughout this module: never compare floats with == (compare with a tolerance instead, abs(a - b) < 1e-9, exactly the pattern every hidden test in this module's code pack uses) and always format floats for display with :.2f (or similar) before showing them to a human, so the noise never reaches a report.
  3. Forgetting that input() (and a line from a file) is always a string. age = input("Enter a price: ") followed by age + 10 raises a TypeError: can only concatenate str (not "int") to str, an honest crash. Worse is the silent version: "3" + "3" evaluates to "33", not 6, with no error at all, because both sides genuinely are valid strings and + genuinely is defined for strings (concatenation). The expert habit: the moment text arrives from input(), a file, or anywhere outside your own program, convert it immediately and deliberately: float(raw.strip()), before it touches a single arithmetic operator, which is precisely this module's parse_price exercise.
  4. Off-by-one errors in indexing and ranges. Indexing from 0, and range(a, b) never including b, are two of the most reliable sources of a beginner bug in any language that shares this convention. The expert habit is not "never make this mistake" (everyone does, occasionally, forever), it is to trace the boundary case by hand every time a loop or index feels the least bit uncertain: for a 4-element list, what is prices[len(prices) - 1]? What does range(1, len(prices)) actually produce? Write out the concrete numbers for a small example before trusting the general code.
  5. Confusing return with print. A function that prints its answer instead of returning it looks entirely correct when you run it and read the console, and fails every single hidden test that calls it and inspects the result, because what actually came back to the caller is None. The expert habit: before submitting any function, ask "if I write result = my_function(...), does result hold the actual answer?", if the only way to see the answer was watching the console, you used print where return belongs.
  6. Mixing tabs and spaces, or misjudging indentation depth. Because indentation is Python's block syntax, an editor that silently mixes tabs and spaces can produce code that looks correctly aligned on screen and still raises an IndentationError, or (more dangerously) code that runs but executes a line one indentation level away from where you intended (a line you meant to be inside an if that is actually one level out, and so always runs). The expert habit: configure your editor to insert spaces (never literal tabs) on the Tab key, and when output looks wrong despite "obviously correct" logic, indentation depth is one of the first things to re-check by eye.
  7. Treating a hardcoded number as if it were the general solution. def total_return(prices): return 0.10 passes a single visible test built on [100, 110] perfectly, and fails immediately the moment a hidden test calls it with any other list, because it is not computing anything; it is reciting one memorized answer. This is not a hypothetical trap invented for this course: it is precisely what this module's hidden tests exist to catch, on both an India and a US series you have not seen the exact numbers for while writing your solution. The expert habit is the deepest one in this whole list: write the formula, not the answer, every function should read like a general procedure that would still be correct if you swapped in a completely different list, because that is the entire point of writing code instead of computing the one number by hand.
  8. Believing "it ran without an error" means "it is correct." A program that raises no exception has cleared the lowest possible bar, the syntax was valid and nothing crashed. It has said nothing at all about whether the answer is right. The expert habit, and the one this whole course is built to install: after "it ran," always ask "does the number it produced match what I'd get computing this by hand on a case I fully understand?", which is exactly why every worked example and every practice problem in this module insists you compute the expected answer yourself, on paper, before you ever run the code that is supposed to reproduce it.

Worked examples

A note on the data: every price figure below is explicitly synthetic (seeded, illustrative (R3)) an NSE-style Indian series (₹, seed 42) and an NYSE-style US series ($, seed 7), the same two series used in this module's code pack (CS1.01), reused deliberately so the teaching and the graded practice reinforce each other. Nothing here is a real historical quote for any named company.

Worked example 1: Your first variables and arithmetic (US, $)

An analyst is handed two adjacent closing prices from a US listing's snapshot: 150.00 yesterday, 151.50 today. Compute the price change and the percentage change, and predict, before running anything, what Python will actually print.

Solution. Step 1: bind names, don't repeat literals. old_price = 150.00; new_price = 151.50. Step 2, the change. change = new_price - old_price → 1.5 exactly (you can check: 151.50 - 150.00 happens to hit a value with a clean binary floating-point representation, so no representation noise appears here, that will not always be true, as the next steps show). Step 3, the percentage change, as a fraction. pct_change = change / old_price → 1.5 / 150.0 = 0.01, again clean. Step 4, as a human-readable percentage. pct_change * 100 → 1.0, so print(f"{pct_change * 100:.2f}%") prints 1.00%. Step 5, predict, then verify, the type of every intermediate value. type(old_price) is float (it has a decimal point); type(change) is also float (a float minus a float is always a float); type(pct_change * 100 >= 1.0) is bool. Read it. Nothing here required running Python at all, every value was computed and predicted by hand first. That is the standing discipline of this entire module: know the answer before the interpreter tells you, so that when they disagree, you know immediately that you, not the machine, made the arithmetic error, and you go find it.

Worked example 2: The input()-returns-a-string trap (India, ₹)

A junior analyst is building a small script that reads one line of raw text (as if freshly typed at an input() prompt, or read from a data file) representing an NSE closing price: raw_line = "3552.00\n" (note the trailing \n, always present on a real line from a file, invisible when you print it). They write next_price = raw_line + 10 intending "add 10 rupees to this price," run it, and get TypeError: can only concatenate str (not "int") to str. Confused, they try total = raw_line + "3560.00" instead: expecting the numeric sum 7112.00, and get "3552.00\n3560.00" instead: no error at all, and a wrong, silently plausible-looking result.

Solution. Step 1, name the type, before touching the logic. raw_line is a str, it is text that looks like a number, but it is not one, and Python will never silently decide otherwise. Step 2: diagnose the crash. raw_line + 10 fails because + between a str and an int is genuinely undefined in Python (unlike some languages, there is no automatic coercion either way), an honest, immediate error, which is the better of the two failures here because it is impossible to miss. Step 3, diagnose the silent bug, which is worse. raw_line + "3560.00" does not crash, because + between two strings genuinely means concatenation. Python is doing exactly what + means for two str values; the bug is not in the operator, it is in the analyst's assumption that these strings "were" numbers. Step 4, the fix, applied once, deliberately. price = float(raw_line.strip()): .strip() first removes the trailing \n (and any stray leading/trailing spaces), then float(...) converts the cleaned text into the actual number 3552.0. Only now do price + 10 (→ 3562.0) and price + 3560.00 (→ 7112.0) do what was intended. Read it. The crash was the easy case, it announced itself. The silent wrong answer is the one that ships to a report unless you have internalized, as a reflex, that anything arriving as text must be deliberately converted before it is treated as a number, precisely the discipline this module's parse_price exercise (CS1.01-e07) exists to drill until it is automatic.

Worked example 3: Classifying a day's move with if/elif/else (US, $)

Using the module's running US series [150.00, 151.50, 148.00, 152.00, 155.00, 154.00, 165.00], classify the third day-over-day move (from 151.50 to 148.00) as "gain", "loss", or "flat", using a 0.05% band around zero for "flat."

Solution. Step 1, compute the fractional change first, always, before branching. pct_change = (148.00 - 151.50) / 151.50 = -3.5 / 151.50 = -0.023102... (about −2.31%). Step 2, trace the if/elif/else by hand, top to bottom, exactly as Python would. First condition: pct_change > 0.0005? -0.0231... > 0.0005 is False, skip this branch entirely, do not evaluate its body, move to the elif. Second condition: pct_change < -0.0005? -0.0231... < -0.0005 is True (Python runs this branch's body (result = "loss") and, critically, never even evaluates the else) once a branch matches, the whole chain is done. Step 3: sanity-check against the threshold's purpose. The 0.05% band exists so that a change of, say, 0.0001 (0.01%), plausibly just floating-point noise from an upstream calculation, not a real move, reads as "flat" rather than as a spurious "gain" or "loss"; a −2.31% move is nowhere near that band, so classifying it decisively as "loss" is exactly right, with no ambiguity. Read it. The discipline worth keeping: trace conditional chains top to bottom, stopping at the first True, exactly the way the interpreter does, never scan an if/elif/else chain looking for "which one is really true," because Python does not do that either; it takes the first match and moves on.

Worked example 4: Counting up-days with a for loop, traced completely (India, ₹)

Using the module's running NSE series [3500.00, 3552.00, 3518.00, 3560.00, 3600.00, 3585.00, 3675.00], hand-trace count_up_days fully, one loop iteration at a time, before ever running it.

Solution. Step 1: set up the trace table. count = 0 to start. The loop is for i in range(1, len(prices)):, with 7 prices, range(1, 7) yields i = 1, 2, 3, 4, 5, 6 (six comparisons for seven prices, one fewer comparison than there are days, because the very first day has no "previous day" to compare against). Step 2: walk every iteration.

iprices[i-1]prices[i]prices[i] > prices[i-1]?running count
13500.003552.00True1
23552.003518.00False1
33518.003560.00True2
43560.003600.00True3
53600.003585.00False3
63585.003675.00True4

Step 3, read off the final answer. After i = 6, the loop ends (because range(1, 7) is exhausted), and return count hands back 4. Step 4, sanity-check the boundary you almost got wrong. Why does the loop start at i = 1, not i = 0? Because at i = 0, prices[i - 1] would be prices[-1]. Python's "last element" syntax, not an error, which makes this exact mistake unusually dangerous: it does not crash, it silently compares the first price to the last one. Read it. This is precisely the loop-tracing discipline every function in this module rewards: build the table, walk it by hand on a small example first, and only then trust the loop to do the same thing automatically on a series ten times longer.

Worked example 5: One function, reused across both markets (India ₹ and US $)

Write total_return(prices) once, and apply the exact same code to both the NSE series and the US series, the payoff of writing a general function instead of computing two separate one-off answers.

Solution. Step 1, write the function once, generally, with no market-specific logic anywhere in it.

def total_return(prices):
    return prices[-1] / prices[0] - 1

Step 2: apply it to India. total_return([3500.00, 3552.00, 3518.00, 3560.00, 3600.00, 3585.00, 3675.00]) → 3675.00 / 3500.00 - 1 = 0.050000000000000044, a 5% total return, with the tiny floating-point noise from Common Mistakes item 2 fully expected and harmless once formatted (f"{0.050000000000000044:.2%}" prints 5.00%, using the % format specifier which multiplies by 100 and appends the sign automatically, a shorthand for f"{x*100:.2f}%"). Step 3, apply it to the US, with zero code changes. total_return([150.00, 151.50, 148.00, 152.00, 155.00, 154.00, 165.00]) → 165.00 / 150.00 - 1 = 0.10000000000000009, a clean 10% total return once formatted. Step 4: name what just happened. Not one line of total_return mentions India, the US, rupees, or dollars, the function is entirely about the shape of the data (an ordered list of prices) and entirely indifferent to what the numbers represent, which is exactly why the same eight lines of logic in this module's code pack are graded against both an India and a US hidden test: a correct, general function has no reason to know or care which market it is looking at. Read it. This is the entire argument for functions in one worked example: you did the real thinking once, and the computer does the arithmetic as many times, on as many inputs, as you ask, correctly, tirelessly, and identically every time, which is precisely what a hand-repeated spreadsheet calculation can never fully guarantee.

Worked example 6: Debugging a small script end to end (India, ₹)

A learner is given three raw lines of text, simulating rows read from a small data file of NSE closes, and writes a script to compute the total return across them:

lines = ["3500.00\n", "3552.00\n", "3675.00\n"]
prices = []
for line in lines:
    prices.append(line)
result = prices[-1] / prices[0] - 1
print(result)

Running it raises TypeError: unsupported operand type(s) for /: 'str' and 'str'. Find and fix the bug using nothing but the error message and the discipline built in this module.

Solution. Step 1, read the error message as data, not as noise. TypeError: unsupported operand type(s) for /: 'str' and 'str' states, precisely, that / was attempted between two str values, which immediately tells you the bug is a type problem, not a logic problem, and tells you exactly which operator failed. Step 2, locate where a str entered the pipeline. lines is a list of strings (each with a trailing \n), that part is expected, since it is standing in for raw file text. The bug is in the loop: prices.append(line) appends the raw string line unchanged; nothing in this script ever calls float(...), so prices ends up as ["3500.00\n", "3552.00\n", "3675.00\n"], a list of strings that look like numbers, not a list of numbers, exactly the trap from Worked example 2, now one layer deeper inside a loop. Step 3, fix at the point of entry, not at the point of failure. The failing line (prices[-1] / prices[0] - 1) is not where the bug lives, it is merely where the wrong type finally got used in an operation that cannot tolerate it. The correct fix is one line earlier: prices.append(float(line.strip())), converting each raw line the moment it enters the list, so every downstream calculation can simply assume prices holds real numbers. Step 4, re-run and confirm the fix against a hand-computed expectation. With the fix, prices is [3500.0, 3552.0, 3675.0], and result is 3675.0 / 3500.0 - 1 = 0.050000000000000044, matching Worked example 5's India figure exactly, which is itself a useful cross-check: two different worked examples computing the same quantity from the same underlying data should always agree. Read it. The general lesson outranks this specific bug: fix data-cleaning problems at the point where dirty data enters your program, not at the point where it finally breaks something, the second location is often several lines away from the first, and chasing only the line in the traceback, without asking "where did this bad value actually originate," is how the same bug reappears in a slightly different shape two lines later.


Practice set

Work every problem fully (on paper, predicting the answer) before reading its solution. Every problem below uses fresh numbers, distinct from the module's running NSE/US series, so you cannot simply recall an answer instead of computing one.

P1 (guided). Type identification. State the type Python assigns to each of the following literals: 42, 42.0, "42", True, None, 3552.00 - 3500.00.

Solution. 42 → int. 42.0 → float (the decimal point makes it a float even though the value is a whole number). "42" → str (quotes make it text, regardless of what the text looks like). True → bool. None → NoneType. 3552.00 - 3500.00 → float (a float minus a float is always a float, here evaluating to 52.0).

P2 (guided). Assignment vs. re-assignment. Trace this code line by line and state the final value of price: price = 100; price = price + 10; price = price * 2.

Solution. Line 1: price is bound to 100. Line 2: the right-hand side is evaluated first using the current value of price (100 + 10 = 110), then price is rebound to 110. Line 3: again the right-hand side evaluates first (110 * 2 = 220), then price is rebound to 220. Final value: 220. (Note this is not "the box called price got multiplied", it is "compute a new value from the current binding, then rebind the name," which matters once aliasing enters the picture in CS1.02.)

P3 (guided). Floor division and modulo. Compute 17 // 5 and 17 % 5 by hand, then explain the relationship between the two results and the original numbers.

Solution. 17 // 5 = 3 (5 goes into 17 three whole times) and 17 % 5 = 2 (the remainder after removing those three whole 5s: 17 - 3*5 = 2). The relationship holds generally: (a // b) * b + (a % b) == a, here 3*5 + 2 = 17. ✓.

P4. Comparison chaining. Without running anything, evaluate 10 < 15 < 20 and 10 < 15 > 20, and state what each one evaluates to and why.

Solution. 10 < 15 < 20 chains as (10 < 15) and (15 < 20) = True and True = True. 10 < 15 > 20 chains as (10 < 15) and (15 > 20) = True and False = False. Chained comparisons in Python always mean "and" between each adjacent pair, read left to right.

P5. String vs. number, once more. What does "7" * 3 evaluate to, and how does it differ from 7 * 3? Explain why Python allows both without raising an error.

Solution. "7" * 3 evaluates to "777", string repetition, a legitimate, separately-defined meaning of * for a str and an int (repeat the string that many times). 7 * 3 evaluates to 21: ordinary multiplication of two ints. Python allows both because * is defined differently depending on the types of its operands (this is called operator overloading); neither is an error, and neither silently becomes the other, the type of each operand fully determines which behavior of * applies.

P6. Tracing an if/elif/else chain. Given classify_move as defined in this module (gain if > 0.0005, loss if < -0.0005, else flat), what does it return for pct_change = 0.0005 exactly? Explain precisely, referencing the operators used.

Solution. "flat". 0.0005 > 0.0005 is False (strictly greater, not "greater or equal"), so the if branch does not run; 0.0005 < -0.0005 is also False, so the elif does not run either; control falls through to else, returning "flat". The boundary value itself always lands in the "otherwise" case whenever both comparisons use strict </> rather than <=/>=, worth checking deliberately any time a threshold value itself might occur in real data.

P7. Hand-trace a loop, fresh numbers. Trace count_up_days([80.0, 76.0, 79.0, 84.0, 82.0]) completely, showing every comparison, and state the final count.

Solution. range(1, 5) yields i = 1,2,3,4. i=1: 76.0 > 80.0? False. i=2: 79.0 > 76.0? True → count=1. i=3: 84.0 > 79.0? True → count=2. i=4: 82.0 > 84.0? False. Final: count = 2.

P8. Total return, fresh numbers (India, ₹). Compute total_return([1000.00, 950.00, 1010.00]) by hand, showing the formula substitution.

Solution. prices[-1] / prices[0] - 1 = 1010.00 / 1000.00 - 1 = 1.01 - 1 = 0.01 (a 1% total return), note the middle price, 950.00 (a 5% intra-period dip), plays no role at all in total_return, which only ever looks at the first and last elements; this is worth noticing precisely because it is easy to assume the middle value "should" matter and it deliberately does not for this particular metric (contrast with average_daily_change, P9-style, which uses every price).

P9. Average daily change vs. total return, fresh numbers (US, $). For [200.0, 210.0, 199.5, 220.0], compute both total_return and average_daily_change, and explain in one sentence why they differ.

Solution. total_return = 220.0/200.0 - 1 = 0.10 (10%). Daily changes: day1 (210-200)/200 = 0.05; day2 (199.5-210)/210 = -0.05; day3 (220-199.5)/199.5 ≈ 0.10276. average_daily_change = (0.05 - 0.05 + 0.10276)/3 ≈ 0.03425 (≈3.43%). They differ because total_return only compares the very first and very last price, while average_daily_change is the arithmetic mean of every individual day's percentage move, two genuinely different, both legitimate, questions about the same data.

P10. Best and worst day, fresh numbers (India, ₹). For [80.0, 76.0, 79.0, 84.0, 82.0] (same series as P7), compute best_and_worst_day, the maximum and minimum single-day percentage changes.

Solution. Daily changes: day1 (76-80)/80 = -0.05; day2 (79-76)/76 ≈ 0.03947; day3 (84-79)/79 ≈ 0.06329; day4 (82-84)/84 ≈ -0.02381. Best = ≈0.06329 (day 3); worst = -0.05 (day 1). Note the worst single day (-5%) is not the only decline in the series (day 4 also fell, by about -2.38%), best_and_worst_day finds the single most extreme move in each direction, not a count of how many days moved which way (that is count_up_days's job instead).

P11. return vs. print. A learner writes def total_return(prices): print(prices[-1] / prices[0] - 1) (no return). They run x = total_return([100, 110]) and it prints 0.1 to the console. What is x bound to afterward, and why does this function fail every hidden test despite "printing the right number"?

Solution. x is bound to None. The function has no return statement, so (regardless of what it printed) it falls off the end and implicitly returns None, exactly per this module's §2. It fails every hidden test because a hidden test calls the function and inspects what came back (total_return([100,110]), then compares that returned value to 0.10), it never looks at console output at all. Printing the right number to a human and returning the right value to the calling code are completely unrelated outcomes.

P12. Parsing raw text. What does parse_price(" 3560.00 \n") return, and what are the two separate operations involved, in order?

Solution. It returns the float 3560.0. Two operations, applied in this exact order: first .strip() removes the leading spaces, the trailing spaces, and the trailing \n, leaving the clean text "3560.00"; then float(...) converts that cleaned string into the number 3560.0. Reversing the order (float before .strip()) would fail, float() cannot parse a string carrying stray whitespace/newline characters around the digits.

P13. Formatting for display. Given pct = 0.023102, what does f"{pct:.2f}%" print, and what does f"{pct:.2%}" print? Explain the difference.

Solution. f"{pct:.2f}%" prints "0.02%": .2f formats the raw number 0.023102 to 2 decimal places (0.02) and the literal % character is simply appended as text, with no scaling. f"{pct:.2%}" prints "2.31%", the % format specifier (no f) additionally multiplies the value by 100 before formatting to 2 decimals, which is what you actually want when pct is a fraction (like 0.023102 meaning 2.3102%). Confusing these two is a real, easy-to-make formatting bug: always ask whether your number is already "in percent" or is a fraction that needs the % specifier's automatic ×100.

P14. Whole-program trace: a while loop. Trace this loop completely and state the final value of years and balance: balance = 1000.0; years = 0; while balance < 1300.0: balance *= 1.10; years += 1.

Solution. Start: balance=1000.0, years=0. Check 1000.0 < 1300.0? True → balance = 1100.0, years = 1. Check 1100.0 < 1300.0? True → balance = 1210.0, years = 2. Check 1210.0 < 1300.0? True → balance = 1331.0000000000002, years = 3. Check 1331.00... < 1300.0? False: loop stops. Final: years = 3, balance ≈ 1331.0 (with the usual harmless floating-point noise past the second decimal, per Common Mistakes item 2).

P15. Spot the bug (indexing). A learner writes def count_up_days(prices): count = 0; for i in range(0, len(prices)): \n if prices[i] > prices[i - 1]: count += 1\n return count (looping from 0, not 1). What goes wrong specifically at i = 0, and is it an error or a silent bug?

Solution. It is a silent bug, not an error. At i = 0, prices[i - 1] becomes prices[-1] (valid Python syntax meaning "the last element") so the loop compares prices[0] (the first price) against prices[-1] (the last price) on its very first pass, an entirely spurious comparison that has nothing to do with "did day 0 rise over the previous day" (day 0 has no previous day). Nothing crashes; the function returns a plausible-looking but wrong count, off by however that spurious extra comparison happened to resolve. The fix is exactly Worked example 4's range(1, len(prices)), starting the comparisons at the first day that genuinely has a predecessor.

P16. AI-generated code, reviewed. An AI coding assistant, asked to write price_change(old_price, new_price), returns: def price_change(old_price, new_price): return old_price - new_price. Is this correct? If not, what is wrong, and how would this module's hidden tests catch it?

Solution. It is wrong: the arguments are subtracted in the wrong order. price_change is specified (§11, and the code pack's CS1.01-e01 prompt) as new_price - old_price, so that a price rise returns a positive number; this draft returns the negative of the correct answer in every case (a rise would incorrectly show as negative, a fall as positive, every sign flipped). This module's hidden tests catch it immediately: price_change(3500.00, 3552.00) is specified to equal 52.0 (a genuine rise from Worked example 4's India series), and this buggy version would return -52.0 instead, failing the assertion outright. This is precisely why the Copilot Discipline in this module's ai-augment-json callouts insists on reviewing every line and running the tests yourself: the bug here is not exotic, it reads as entirely plausible code, and only actually running it against a known-correct example (exactly the discipline of every worked example in this module) catches it.


Applied mini-project

Build a one-page return & growth mini-report, for both markets.

Using this module's two synthetic bundled series: nse_prices = [3500.00, 3552.00, 3518.00, 3560.00, 3600.00, 3585.00, 3675.00] (₹) and us_prices = [150.00, 151.50, 148.00, 152.00, 155.00, 154.00, 165.00] ($), write one small Python program (in this module's Practice/code area, or in any Python you have installed) that, for each market, computes and prints:

  1. The total return over the full run (total_return).
  2. The number of up-days (count_up_days).
  3. The average daily change (average_daily_change), and one sentence (computed, not guessed) explaining why it differs from the total return.
  4. The best and worst single day (best_and_worst_day).
  5. A final, clean formatted report line for each market (format_report), suitable for a real analyst to read without needing to see any raw code or raw floats.

This is deliberately the integration task: every piece above is a graded, individually-tested function in this module's code pack (CS1.01); the mini-project is where you wire all eight functions together into one small, coherent program and confirm the whole pipeline (from a raw list of prices to a clean, human-readable report) actually works end to end, for both an India and a US series, without you re-deriving any formula from scratch. Write the program by hand, run it, and (before you trust a single number) recompute at least the total return and the up-day count for both markets yourself, on paper, exactly as the Worked examples did, and confirm your program agrees.

Self-check rubric, score yourself out of 10, pass at 8/10 with no zero on any item:

  • (2 pts) Both markets present and correctly labeled, the report clearly distinguishes the ₹ NSE figures from the $ US figures, with no mixed-up numbers.
  • (2 pts) Every one of the five computed quantities is present for both markets, total return, up-day count, average daily change, best/worst day, and the formatted line; nothing skipped.
  • (2 pts) At least the total return and up-day count were independently hand-verified (shown as a written trace, not just "I ran it and it looked fine") for at least one market, before being trusted.
  • (2 pts) The average-daily-change vs. total-return sentence is numerically correct and specific, it states why they differ in terms of what each formula actually looks at (first-and-last vs. every day), not a vague gesture at "rounding."
  • (2 pts) All output is cleanly formatted (:.2f / :.2%, no raw floating-point noise like 0.050000000000000044 visible anywhere in the final printed report).

Keep this program, the "compute by hand, write the general function, verify by re-computing" loop you just ran end to end is the exact discipline every later module in this branch, and every one of the CS1.01 code-pack's own hidden tests, will hold you to.


Reading & resources

Everything below supports what this module teaches; none of it substitutes for actually writing and running the code yourself. Free unless marked; tags show the ramp from first-contact to depth.

  • Python's own official tutorial, The Python Tutorial (docs.python.org), the primary, authoritative reference for every syntax rule in this module (types, control flow, functions), written by the language's own maintainers. [Free] [Beginner]
  • Al Sweigart, Automate the Boring Stuff with Python, the friendliest possible on-ramp for a true beginner, with exactly this module's spirit: small, practical programs from day one. [Free online] [Beginner]
  • CS50 (Harvard, via edX/CS50.dev), a rigorous, structured "programming from zero" course covering this module's ground (and beyond) with genuine computer-science depth; a strong companion track if you want a second explanation of the same fundamentals. [Free] [Beginner]
  • Real Python (realpython.com), deep, example-driven articles on individual Python topics (f-strings, floating-point representation, list basics); best used to go one level deeper on any single idea from this module once you have the fundamentals. [Free tier / Paid membership for full library] [Beginner–Intermediate]
  • MIT The Missing Semester of Your CS Education, not this module's content directly, but the natural next stop once you are comfortable with Python itself and want the surrounding practitioner skills (the command line, environments); previews what CS2.01 will formalize. [Free] [Beginner–Intermediate]
  • Luciano Ramalho, Fluent Python: the deep, idiomatic-Python reference this module deliberately does not try to be; the right book once CS1.02 opens the door to comprehensions, mutability, and idiomatic classes. [Paid] [Intermediate–Advanced]: (a CS1.02-level anchor, named here so you know where this module's "first taste" leads next.)

Do this, not just read. Open any Python environment (this module's own code editor is enough) and, for ten minutes, deliberately break things on purpose: divide by zero and read the traceback; compare two floats with == and watch it fail unexpectedly; print 0.1 + 0.2; index a 3-element list at position 5 and read the IndexError. Learning to read Python's own error messages calmly, as precise diagnostic information rather than as a verdict on your competence, is worth more than any amount of passive reading, and every one of those five small experiments is something you will hit for real, unannounced, before this module's code pack is done.


Teach it back & journal

Feynman prompt. Explain to someone who has never written a line of code why def total_return(prices): return prices[-1] / prices[0] - 1 is a genuinely complete answer to "what was the return on this investment," for any list of prices you hand it, not just the one example you tested it on. Use, in your own words, the idea that a function computes a general formula rather than reciting a memorized answer, and that this module's hidden tests exist purely to prove the difference. Then explain, separately, why input() handing back the text "165.50" instead of the number 165.5 is not a Python design flaw but a deliberate refusal to guess what you meant. If your listener can, unprompted, predict that a function which only prints its answer instead of returning it will fail an automated test even though it "looks right" when they run it, you have installed this module's central discipline.

Journal prompt. Over the next few days, keep a short log of every error message Python actually shows you while you work through this module's code pack: the exact wording, which line you thought was the problem, and which line the error actually revealed once you read it carefully. How often did the true bug live somewhere other than the line the traceback pointed at? How many of your bugs were a silent wrong answer (no error at all) rather than a crash, and how did you catch those, if not from an error message? Then write a short paragraph: which single habit from this module, hand-tracing a loop before trusting it, converting text to numbers the moment it arrives, or writing the general formula instead of chasing one visible example, would most change how confidently you'd hand a piece of your own code to someone else to rely on, and what will you do differently on your very next function because of it?

Advertisement

The gate

The module's flashcards and its quiz are in the app. The quiz is the gate: you need 85% to pass it, and the next module opens when you do.