Workshop · Python · Beginner
PY050 Python from zero
Python taught the way working programmers use it: a small, honest program in every step, run from the terminal in your own VS Code seat, with the AI tutor beside you. Forty short steps across eight sessions take you from printing a line to a tested command-line expense tracker that reads and writes real files, and every step ends with a check the machine runs on your own code.
Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick PY050 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.
Who it is for
Adults and teens who have never programmed. You can type, you can follow a recipe, and you want to make a computer do something useful by the end of the first hour. No maths beyond percentages.
Where it leads. JV100 Java I · DN100 C# I: the working programmer’s toolkit · PY100 Python I · RS100 Rust I
What you will be able to do
- Run a Python file from the terminal, read its output and read any error message back to the line it names
- Hold text, whole numbers, decimals and money in the right type, and explain why 0.1 + 0.2 is not 0.3
- Format a column of numbers with f-strings so a human can read it
- Choose with if/elif/else and repeat with for and while, without an off-by-one
- Keep things in lists and dictionaries, walk them, sort them and count them
- Write a function with inputs and a return value, and test it with pytest
- Read and write text and CSV files, closing them properly, and survive a missing file
- Build a small command-line program with arguments and an exit-code contract
Syllabus
Every session ends in a lab the machine checks on your own code. Open a session's mini steps to read each lesson, demo and hands-on task; the badge shows how far the engine can verify it: checked · T3 a check runs over your files, proven · T4 the grader also tries inputs you did not see, tutored · T0 done on your word. Each step has its own link (for example #s1-1).
- 1. Your first programs
- 2. Data: text, numbers and money
- 3. Computing and formatting
- 4. Choices and repetition
- 5. Lists and dictionaries
- 6. Functions and tests
- 7. Files and errors
- 8. Capstone: Tally, the expense tracker
Session 1 · Your first programs
Five steps that make the seat yours. What a Python program is: a text file the interpreter reads from the top. The rhythm of the course — run, read, change, run again. Printing text and numbers, and the difference between the two. Reading an error message back to the line it names instead of guessing. Live in the seat: a file run from the terminal, a deliberate typo and its traceback, three ways to break print and what each one says, and the first saved program with a comment explaining what it does.
Lab Write greet.py: it prints a three-line banner, a sentence that uses a number, and a line built from two pieces of text joined together. It must run clean on the first try after you have read your own code once. Session 1 checkpoint.
5 mini steps · ~39 min
1.1Run, read, change8 minchecked · T3
Predict
A file hello.py contains one line: print("Hello"). What appears when you run python3 hello.py?
Lesson
A Python program is a plain text file; python3 reads it from the first line to the last and does what each line says. You will learn everything in one rhythm: run the file, read what came out, change one thing, run it again.
Demo
In the seat the tutor opens python/py050/demo/hello_demo.py, runs python3 hello_demo.py in the terminal, points at the output, changes the word inside the quotes, runs again, and shows that the terminal keeps the old output above the new.
On screen
Explorer → python/py050/demo/hello_demo.py → Terminal → python3 hello_demo.py → edit the string → run again.
Hands-on
Create python/py050/s1/hello.py with one print line of your own words (not Hello), run it, then add a second print line and run it again so the terminal shows both runs.
Explain
In one sentence: what did python3 do with your file?
Check
hello.py prints two lines of your own — checked (T3):
seat_fileseat_run1.2Text and numbers are different things7 minchecked · T3
Predict
What does print(2 + 3) print, and what does print("2" + "3") print?
Lesson
Python keeps text (str) and numbers (int, float) as different kinds of value and the same symbol does different work on each: + adds numbers but joins text. type() tells you what you have.
Demo
The tutor types five expressions in the Python REPL in the terminal — 2 + 3, "2" + "3", "2" * 3, 7 / 2, 7 // 2 — and reads each result aloud, then calls type() on two of them.
Hands-on
In python/py050/s1/types.py print four lines: the sum 40 + 2, the text "40" + "2", the text "ab" * 3, and type(42). Run it and compare with your predictions.
Explain
Why did "40" + "2" not give 42?
Check
types.py prints 42, 402, ababab and the int type — checked (T3):
seat_run1.3Reading an error message8 minchecked · T3
Predict
A file has print("Hi" on line 3 with the closing bracket missing. Which line will the error message name?
Lesson
An error message is a map, not a verdict. The last line says what kind of error; the lines above say where. Go to the file and line it names first, read that line slowly, then think about the reason.
Demo
The tutor breaks python/py050/demo/broken_demo.py three ways in turn — a missing bracket, a misspelt print, a number added to text — runs each, and reads the traceback from the bottom up, pointing at the file:line each time.
Hands-on
Copy python/py050/samples/s1/oops.py to python/py050/s1/fixed.py. It has three errors. Fix them one at a time by reading the message, until it prints the two lines it was meant to print: "Total: 12" and "Done".
Explain
Which line of a traceback do you read first, and why?
Check
fixed.py runs clean and prints Total: 12 and Done — checked (T3):
seat_run1.4Comments, names and the shape of a file7 minchecked · T3
Predict
After price = 5 and then price = 7, what does print(price) show?
Lesson
A variable is a name you give a value so you can use it again or replace it. A comment starts with # and is for people, not Python. A tidy file has a one-line comment at the top, names that say what they hold, and one idea per line.
Demo
The tutor writes python/py050/demo/names_demo.py: a comment, three variables (item, qty, price), a print that uses all three, then renames a variable badly (x) and well (unit_price) to show how the second reads a month later.
Hands-on
Write python/py050/s1/receipt_line.py with a header comment and variables item (text), qty (whole number) and unit_price (decimal); print one line like "3 x Notebook at 2.5" built from them.
Explain
Why is a comment not an instruction?
Check
receipt_line.py uses three named variables and a comment — checked (T3):
seat_fileseat_run1.5Lab: the greeting banner9 minchecked · T3
Predict
print("Py" + "thon", 3) prints what?
Lesson
Everything from this session in one small program: prints, text joined with +, a number used in a sentence, a header comment, and the discipline of reading your own file once before you run it.
Demo
The tutor builds python/py050/demo/banner_demo.py live: three banner lines of = and a title, a sentence with a number, a line from two joined strings; runs it; then shows the lab's check output so the learner knows what the grader reads.
Hands-on
Write python/py050/s1/greet.py: a comment on line 1; a banner of exactly three lines (a line of = signs, a title line, a line of = signs); one sentence that includes the number 5 computed as 2 + 3; and one line made by joining two strings with +. Run it clean on the first try.
Explain
Name one thing you now check in a file before you run it.
Check
greet.py: three-line banner, a computed 5, a joined line — checked (T3):
seat_fileseat_run
Session 2 · Data: text, numbers and money
Five steps on the values a program holds. Text as a sequence of characters you can measure, cut and search. Whole numbers that are exact and decimals that are not — why 0.1 + 0.2 is not 0.3 in any binary language. Converting between text and numbers on purpose, and the error you get when the text is not a number. The flagship step: what survives when a number goes into a differently shaped home — int() drops the fraction, round() rounds, Decimal keeps the digits you typed — and why money is held as cents or Decimal, never as float. Live in the seat: a string sliced three ways, 0.1 + 0.2 printed to twenty places, int("4.5") failing, and the same 12345.67 pushed through four conversions.
Lab Write receipt.py: three fixed items with quantities and unit prices held in cents, a line per item and a total in dollars and cents, computed without a single float. The printed total must be exactly 47.25. Session 2 checkpoint.
5 mini steps · ~40 min
2.1Text you can measure and cut7 minchecked · T3
Predict
word = "notebook". What is word[0:4]?
Lesson
A string is a row of characters with positions starting at 0. len() measures it, square brackets pick a character or a slice, and methods such as .upper(), .strip() and .replace() give you a changed copy, never the original.
Demo
The tutor opens python/py050/demo/strings_demo.py and prints len("notebook"), "notebook"[0], "notebook"[-1], "notebook"[0:4], " hi ".strip() and "a-b-c".replace("-", "+"), reading each result as it appears.
Hands-on
Write python/py050/s2/cut.py: set title = "agile system software" and print its length, its first five characters, the last word (slice from position 13), and the whole title in upper case — four lines in that order.
Explain
Why does title.upper() not change title?
Check
cut.py prints 21, agile, software, AGILE SYSTEM SOFTWARE — checked (T3):
seat_run2.2Whole numbers are exact, decimals are not8 minchecked · T3
Predict
What does print(0.1 + 0.2 == 0.3) show?
Lesson
int holds whole numbers exactly, as large as you like. float holds decimals in binary, so most decimal fractions are approximations. Use // for whole division and % for the remainder; / always gives a float.
Demo
In python/py050/demo/floats_demo.py the tutor prints 0.1 + 0.2, then the same with twenty decimal places using format, then 7 / 2, 7 // 2, 7 % 2 and 2 ** 10, pausing on the surprising one.
Hands-on
Write python/py050/s2/exact.py that prints 0.1 + 0.2, then 17 // 5, then 17 % 5, then 2 ** 8 — one per line. Look at the first line and say out loud why it is not 0.3.
Explain
In one sentence, why can a float not hold 0.1 exactly?
Check
exact.py shows the float surprise and three exact results — checked (T3):
seat_run2.3Converting on purpose7 minchecked · T3
Predict
What happens with int("4.5")?
Lesson
int(), float() and str() convert between types when you ask. Text that is not a valid number raises ValueError, and the message names the text it refused. Python never converts silently between text and numbers.
Demo
The tutor runs python/py050/demo/convert_demo.py: int("42") + 1, str(42) + "1", float("2.50"), then int("4.5") to show the ValueError and read its last line aloud.
Hands-on
Write python/py050/s2/convert.py: qty_text = "12" and price_text = "2.50"; print the product of the two converted numbers (30.0), then print str(int(qty_text) * 2) + " items" (24 items). Then add a third line that converts "ten" with int(), run it, and read the error.
Explain
What did the ValueError message tell you?
Check
convert.py prints 30.0 and 24 items, then fails on int("ten") — checked (T3):
seat_runseat_file2.4What survives: int, round and Decimal9 minchecked · T3
Predict
gross = 12345.67. What does print(int(gross)) show?
Lesson
When a number moves into a different shape, the shape decides what survives. int() truncates toward zero, round() rounds to the places you name, and Decimal keeps exactly the digits you typed. Money lives in Decimal or in integer cents, never in float.
Demo
In python/py050/demo/shapes_demo.py the tutor pushes 12345.67 through int(), round(..., 1), round(..., -2) and Decimal("12345.67").quantize(Decimal("0.1")), then adds Decimal("0.10") ten times and shows it equals Decimal("1.00").
Hands-on
Write python/py050/s2/shapes.py: from decimal import Decimal; amount = Decimal("12345.67"); print int(amount), round(amount, 1), amount.quantize(Decimal("1")), and the sum of Decimal("0.10") added ten times — four lines.
Explain
Why is Decimal, not float, used for money here?
Check
shapes.py prints 12345, 12345.7, 12346, 1.00 — checked (T3):
seat_fileseat_run2.5Lab: the receipt in cents9 minchecked · T3
Predict
total_cents = 4725. What does print(total_cents // 100, total_cents % 100) show?
Lesson
Integer cents make money arithmetic exact with nothing but int. Multiply quantity by unit cents, add the lines, then split the total into dollars and cents with // and % only when you print.
Demo
The tutor builds python/py050/demo/receipt_demo.py with two items, computes line totals in cents, prints each line and the total as dollars.cents, then deliberately changes a price to a float to show 0.1-style noise creep in.
Hands-on
Write python/py050/s2/receipt.py with three items: Notebook 3 × 250 cents, Pen 5 × 125 cents, Folder 2 × 1700 cents. Print one line per item ending in its line total in cents, then "Total: 47.25" built with // and %, zero-padding the cents. No float anywhere.
Explain
Why did you keep cents until the final print?
Check
receipt.py totals 47.25 from integer cents — checked (T3):
seat_fileseat_runcraft
Session 3 · Computing and formatting
Five steps on getting numbers right and making them readable. Arithmetic and precedence, with the brackets that make intent obvious. f-strings as the one way to build output text, with expressions inside the braces. Widths, alignment and thousands separators so a column of money lines up. Rounding on purpose: Python's round() rounds half to even, Decimal.quantize rounds the way a shop does, and you choose. Live in the seat: a percentage computed three ways with three different answers, a table of five prices aligned to the cent, and round(2.5) printing 2.
Lab Write tip.py: it takes a bill amount from the command line, computes a 15% tip rounded half up to the cent with Decimal, and prints the bill, tip and total as three aligned lines. Session 3 checkpoint.
5 mini steps · ~39 min
3.1Arithmetic and precedence7 minchecked · T3
Predict
What is 100 + 50 * 2 / 100?
Lesson
Python follows the arithmetic you learned at school: brackets, then powers, then multiply and divide, then add and subtract, left to right. When a formula matters, add brackets even when they are not needed; they tell the reader what you meant.
Demo
In python/py050/demo/precedence_demo.py the tutor computes a 12% increase on 250 three ways — 250 + 250 * 12 / 100, (250 + 250) * 12 / 100 and 250 * (1 + 12 / 100) — and shows which two agree and why.
Hands-on
Write python/py050/s3/percent.py that prints the price 250 increased by 12% (280.0), then the price 250 after a 12% discount (220.0), then what percentage 45 is of 180 (25.0) — each with brackets that show the intent.
Explain
Why add brackets a computer does not need?
Check
percent.py prints 280.0, 220.0, 25.0 — checked (T3):
seat_run3.2f-strings: text with values inside7 minchecked · T3
Predict
qty = 3; print(f"{qty} x {qty * 2}") shows?
Lesson
An f-string is text with holes; anything inside the braces is evaluated and inserted. It replaces + and str() for building output, keeps the sentence readable, and after a colon you can say how the value should look.
Demo
The tutor rewrites python/py050/demo/receipt_demo.py from session 2 using f-strings, then shows {price:.2f}, {qty:>3} and {name:<10} one at a time with the output beside each change.
Hands-on
Write python/py050/s3/fstring.py with name = "Folder", qty = 2 and cents = 3400; print f"{qty} x {name}" and then f"Line total: {cents / 100:.2f}" — two lines: "2 x Folder" and "Line total: 34.00".
Explain
What does :.2f after a value do?
Check
fstring.py prints two formatted lines — checked (T3):
seat_fileseat_run3.3Columns that line up8 minchecked · T3
Predict
print(f"[{1234567:,}]") shows?
Lesson
Inside the braces, after the colon: a width pads to that many characters, < > and ^ align left, right and centre, a comma adds thousands separators, and .2f fixes two decimals. Right-align numbers and left-align names and a table reads itself.
Demo
In python/py050/demo/columns_demo.py the tutor prints five products with {name:<12}{qty:>4}{price:>10,.2f}, first without widths (a ragged mess), then with them, and shows how changing one width shifts the whole column.
Hands-on
Write python/py050/s3/table.py that prints a header and three rows for Notebook 3 at 2.50, Pen 12 at 1.25 and Desk 1 at 1250.00 using widths <10 for the name, >4 for qty and >10,.2f for price — the Desk row must show 1,250.00.
Explain
Why right-align numbers and left-align names?
Check
table.py prints an aligned table with 1,250.00 — checked (T3):
seat_run3.4Rounding on purpose8 minchecked · T3
Predict
What does print(round(2.5), round(3.5)) show?
Lesson
round() rounds half to even, which is right for statistics and wrong for a till. Decimal's quantize with ROUND_HALF_UP rounds the way customers expect. Round once, at the end, to the places you name, and say which rule you used.
Demo
The tutor runs python/py050/demo/rounding_demo.py: round(2.5), round(0.125, 2), then Decimal("2.5").quantize(Decimal("1"), rounding=ROUND_HALF_UP) and the same for "0.125" to two places, laying the four answers side by side.
Hands-on
Write python/py050/s3/round_up.py: import Decimal and ROUND_HALF_UP; for the three amounts "2.345", "2.355" and "10.005" print each quantized to Decimal("0.01") with ROUND_HALF_UP, one per line.
Explain
When is half-to-even the right rule?
Check
round_up.py prints 2.35, 2.36, 10.01 — checked (T3):
seat_fileseat_run3.5Lab: the tip calculator9 minchecked · T3
Predict
A 15% tip on 48.30, rounded half up to the cent, is?
Lesson
One program that uses everything in this session: a bill read from the command line (one line you will fully understand in session 8), Decimal arithmetic, half-up rounding to the cent, and three aligned output lines.
Demo
The tutor writes python/py050/demo/tip_demo.py: bill = Decimal(sys.argv[1]), tip = (bill * Decimal("0.15")).quantize(...), total = bill + tip, then f-strings with {label:<6}{value:>10.2f}; runs it with 48.30 and with 100.
Hands-on
Write python/py050/s3/tip.py: take the bill from sys.argv[1] as a Decimal, compute a 15% tip quantized half up to 0.01, and print three lines "Bill", "Tip" and "Total" each padded so the amounts right-align in a 10-wide column. With 48.30 the tip line shows 7.25 and the total 55.55.
Explain
Why quantize the tip before adding it to the bill?
Check
tip.py 48.30 → Bill 48.30, Tip 7.25, Total 55.55 — checked (T3):
seat_fileseat_run
Session 4 · Choices and repetition
Five steps on control flow. if/else as a fork in the road, with the indented block as the thing Python actually runs. elif for a ladder of cases, comparisons that must compare like with like, and and/or/not combining conditions. for over a range to repeat a known number of times; while for as long as something holds, and the off-by-one that bites everyone once. Live in the seat: a grade ladder tested at every boundary, a loop that prints the twelve-times table, a while loop that runs one time too many and the fix, and a quiz that scores its answers.
Lab Write quiz.py: three fixed questions with known answers, the learner's answers taken from the command line, a point for each correct answer compared case-insensitively, and a final score line with a verdict. Session 4 checkpoint.
5 mini steps · ~39 min
4.1A fork in the road: if and else7 minchecked · T3
Predict
age = 17. After if age >= 18: print("adult") else: print("minor"), what prints?
Lesson
if tests a condition that is True or False and runs its indented block only when it is True; else runs otherwise. The colon and the indentation are the syntax: everything indented under the if belongs to it.
Demo
In python/py050/demo/ifelse_demo.py the tutor writes a temperature check, runs it with three values, then un-indents a line to show it now runs every time regardless of the condition.
Hands-on
Write python/py050/s4/stock.py: qty = 3; if qty is 0 print "Out of stock", otherwise print "In stock: 3" using an f-string. Then change qty to 0, run again, and set it back to 3.
Explain
How does Python know which lines belong to the if?
Check
stock.py branches on qty — checked (T3):
seat_fileseat_run4.2Ladders and combined conditions8 minchecked · T3
Predict
score = 90 in a ladder: if score >= 90 "A", elif score >= 80 "B", else "C". What prints?
Lesson
elif adds rungs to a ladder; the first True rung runs and the rest are skipped, so order matters. and, or and not combine conditions, and comparing text with a number is a bug the comparison will not hide for long.
Demo
The tutor builds the grade ladder in python/py050/demo/ladder_demo.py, tests 90, 89, 80 and 79 at the boundaries, then reverses two rungs to show every score become a C.
Hands-on
Write python/py050/s4/shipping.py: weight = 2.5 and express = True. Print "Free" if weight < 1 and not express, "Express" if express, else "Standard". Then print a second line for weight 0.5 and express False using the same ladder copied below.
Explain
Why does the order of elif rungs matter?
Check
shipping.py prints Express then Free — checked (T3):
seat_fileseat_run4.3for and range: a known number of times7 minchecked · T3
Predict
How many lines does for i in range(3): print(i) produce, and what is the last one?
Lesson
for repeats its block once per item; range(n) gives the numbers 0 up to but not including n, and range(a, b) from a up to but not including b. The loop variable takes each value in turn.
Demo
In python/py050/demo/for_demo.py the tutor prints range(5), then range(1, 6), then the twelve-times table with an f-string, pausing on the first and last line to show where range stops.
Hands-on
Write python/py050/s4/table7.py that prints the seven-times table from 1 x 7 = 7 to 10 x 7 = 70, one line each, in the exact form "3 x 7 = 21".
Explain
Why does range(10) not include 10?
Check
table7.py prints 10 lines from 1 x 7 to 10 x 7 — checked (T3):
seat_fileseat_run4.4while and the off-by-one8 minchecked · T3
Predict
n = 0; while n < 3: n = n + 1; then print(n). What prints?
Lesson
while repeats as long as its condition holds and checks it before every run. Something in the body must move the condition toward False, or the loop never ends. Off-by-one errors live at the boundary: test the first and last value by hand.
Demo
The tutor writes a savings loop in python/py050/demo/while_demo.py — add 50 until the balance reaches 300 and count the months — gets 7 instead of 6 with <=, prints the balance each pass, and fixes the condition.
Hands-on
Write python/py050/s4/double.py: balance = 1000; count the years until a 7% yearly increase makes the balance at least 2000, using a while loop; print "Years: 11" and the final balance to two decimals.
Explain
How did you check for an off-by-one?
Check
double.py counts 11 years to double at 7% — checked (T3):
seat_fileseat_run4.5Lab: the quiz9 minchecked · T3
Predict
"Paris".lower() == "paris" is?
Lesson
The quiz joins a loop with a choice: for each question compare the given answer with the right one, ignoring case, and add to a score. A final ladder turns the score into a verdict.
Demo
The tutor builds python/py050/demo/quiz_demo.py with two questions, answers from sys.argv[1:], a for over range(len(...)), .lower() on both sides, and a score line; runs it right, wrong and with shouted answers.
Hands-on
Write python/py050/s4/quiz.py with three questions whose answers are 8, paris and python. Read three answers from sys.argv[1:], score them case-insensitively, print "Correct" or "Wrong" per question, then "Score: 3/3" and "Perfect" for 3, "Good" for 2, else "Keep practising".
Explain
Why compare lower-cased copies instead of the originals?
Check
quiz.py scores 8 PARIS python as 3/3 Perfect and 8 rome java as 1/3 — checked (T3):
seat_fileseat_runseat_run
Session 5 · Lists and dictionaries
Five steps on holding many things at once. A list as an ordered row you index, append to and walk. Walking with for, with enumerate when you need the position, and the in test. A dictionary as a lookup from key to value, how to add, read with a default and loop over its items. Sorting with sorted() and a key, counting with a dictionary, and the difference between a list of names and a list of records. Live in the seat: a list grown from empty, a dictionary of prices with a missing key and the KeyError it raises, a shopping list sorted three ways, and category counts built in a loop.
Lab Write shopping.py: a fixed list of items each with a category and a price in cents; print the items sorted by name, the number of items per category, and the most expensive item. Session 5 checkpoint.
5 mini steps · ~39 min
5.1A list of things7 minchecked · T3
Predict
items = ["pen", "ink"]; items.append("pad"); print(items[2], len(items))
Lesson
A list holds values in order. Square brackets make one, [i] reads position i (from 0), .append() adds to the end, len() counts, and in asks whether a value is present. Lists change in place, unlike strings.
Demo
In python/py050/demo/list_demo.py the tutor starts from [] and appends three items, prints the list, items[0], items[-1], len(items), "ink" in items, then reads items[5] to show the IndexError.
Hands-on
Write python/py050/s5/cart.py: start with an empty list, append "pen", "ink" and "pad", then print the list, its length, its last item, and whether "ink" is in it — four lines.
Explain
What is the difference between items[-1] and items[len(items)]?
Check
cart.py builds and inspects a three-item list — checked (T3):
seat_fileseat_run5.2Walking a list7 minchecked · T3
Predict
for i, name in enumerate(["a", "b"], start=1): print(i, name). The first line is?
Lesson
for item in items visits each value in order; enumerate(items) gives the position too. Build a total by starting a variable at 0 before the loop and adding inside it, and never change a list while you are walking it.
Demo
The tutor sums a list of cents in python/py050/demo/walk_demo.py, then prints a numbered list with enumerate, then shows the classic mistake of writing total = price inside the loop instead of total += price.
Hands-on
Write python/py050/s5/numbered.py: prices = [250, 125, 1700]; print a numbered line per price starting at 1 in the form "1. 250", then "Total: 2075".
Explain
Why must the total start at 0 before the loop?
Check
numbered.py lists three prices and totals 2075 — checked (T3):
seat_fileseat_run5.3Dictionaries: looking things up8 minchecked · T3
Predict
prices = {"pen": 125}; print(prices.get("ink", 0)) shows?
Lesson
A dictionary maps keys to values: curly braces, key: value pairs, [key] to read or set, .get(key, default) to read without an error, and for key, value in d.items() to walk it. Keys are unique; setting one again replaces its value.
Demo
In python/py050/demo/dict_demo.py the tutor builds a price dictionary, reads two keys, reads a missing one with [ ] to show the KeyError, then with .get, then walks it with .items() printing name and price.
Hands-on
Write python/py050/s5/prices.py: a dictionary of pen 125, ink 450, pad 300. Print the price of ink, then prices.get("desk", 0), then set pad to 350 and print one "name: price" line per item in insertion order.
Explain
When would you use [key] rather than .get?
Check
prices.py reads, defaults, updates and walks a dictionary — checked (T3):
seat_fileseat_run5.4Sorting and counting8 minchecked · T3
Predict
sorted(["pad", "Pen", "ink"]) gives?
Lesson
sorted() returns a new sorted list and leaves the original alone; key= names a function that picks what to sort by, reverse=True flips it. Counting by category is a dictionary whose values start at 0 and grow inside a loop.
Demo
The tutor sorts a list of (name, price) tuples in python/py050/demo/sort_demo.py by name, by price and by price descending with lambda, then counts words in a list with counts[word] = counts.get(word, 0) + 1.
Hands-on
Write python/py050/s5/count.py: words = ["pen", "ink", "pen", "pad", "pen", "ink"]; print the sorted unique words on one line joined by spaces, then one "word count" line per word in alphabetical order.
Explain
What does key= do in sorted?
Check
count.py prints the unique words and counts — checked (T3):
seat_fileseat_run5.5Lab: the shopping list9 minchecked · T3
Predict
max([("pen", 125), ("desk", 12500)], key=lambda t: t[1]) returns?
Lesson
A list of records — here (name, category, cents) tuples — plus sorting by a key, counting into a dictionary and max with a key gives you a real report in twenty lines.
Demo
The tutor writes python/py050/demo/shop_demo.py with four records, prints them sorted by name with aligned columns, counts per category, and finds the dearest with max(key=...), narrating the key function each time.
Hands-on
Write python/py050/s5/shopping.py with items: ("notebook", "paper", 250), ("pen", "writing", 125), ("desk", "furniture", 12500), ("ink", "writing", 450), ("folder", "paper", 1700). Print the names sorted alphabetically one per line, then "category: count" lines in alphabetical order, then "Most expensive: desk".
Explain
Why hold each item as a tuple rather than three parallel lists?
Check
shopping.py: sorted names, category counts, dearest item — checked (T3):
seat_fileseat_run
Session 6 · Functions and tests
Five steps that turn scripts into pieces you can trust. def to name a piece of work, parameters as its inputs and return as its answer — and the difference between returning a value and printing it. Default arguments and keyword arguments. Scope: why a function's variables vanish when it returns and why that is good. pytest: a test file, a test function, an assert, and the red-green rhythm. Live in the seat: a function that prints instead of returning and the None that leaks out, a budget function tested at its boundary, and pytest run from the terminal with one failure read aloud.
Lab Write budget.py with three pure functions — remaining, percent_used and over_budget — and test_budget.py with at least four tests that pass. The grader adds hidden tests of its own. Session 6 checkpoint.
5 mini steps · ~41 min
6.1def and return7 minchecked · T3
Predict
def double(n): print(n * 2) then x = double(4); print(x). What prints on the second line?
Lesson
def names a block of code and its parameters; calling it runs the block with those values; return hands a result back to the caller. A function that only prints gives its caller None — the difference matters the moment you test it.
Demo
In python/py050/demo/func_demo.py the tutor writes area(width, height) with return, calls it twice, then rewrites it with print instead and shows x = area(2, 3) holding None.
Hands-on
Write python/py050/s6/area.py with a function area(width, height) that returns width * height; call it for 3 x 4 and 2.5 x 2 and print both results, then print area(1, 1) + area(2, 2) — three lines: 12, 5.0, 5.
Explain
Why return instead of print inside a function?
Check
area.py defines and uses area() — checked (T3):
seat_fileseat_run6.2Parameters, defaults and pure functions8 minchecked · T3
Predict
def tip(bill, rate=15): return bill * rate / 100. What is tip(200)?
Lesson
A parameter may carry a default, and callers can name arguments: tip(200, rate=20). A pure function uses only its inputs and returns a result without printing or touching anything outside — the same inputs always give the same answer.
Demo
The tutor gives tip() a default rate in python/py050/demo/pure_demo.py, calls it positionally and by keyword, then shows an impure version that reads a global total and changes it, and why its result depends on history.
Hands-on
Write python/py050/s6/discount.py: a pure function price_after(cents, percent=10) returning the discounted integer cents using // ; print price_after(2000), price_after(2000, 25) and price_after(cents=999, percent=50) — 1800, 1500, 499.
Explain
What makes a function pure?
Check
discount.py uses a default and keyword arguments — checked (T3):
seat_fileseat_run6.3Scope: where a name lives7 minchecked · T3
Predict
total = 0; def add(n): total = n; add(5); print(total). What prints?
Lesson
Names assigned inside a function are local to it and disappear when it returns; the function can read outer names but assigning makes a new local one. Pass values in and return values out instead of reaching for globals.
Demo
In python/py050/demo/scope_demo.py the tutor shows a local shadowing a global, prints the outer value unchanged, then refactors to return the new total and assign it outside.
Hands-on
Write python/py050/s6/scope.py: balance = 100; a function deposit(balance, amount) that returns the new balance; call balance = deposit(balance, 50) and print balance; then print the result of deposit(0, 1) — lines 150 and 1. No global statement.
Explain
Why avoid the global keyword here?
Check
scope.py passes and returns instead of using global — checked (T3):
seat_fileseat_run6.4Your first pytest test9 minchecked · T3
Predict
A test file test_area.py has def test_square(): assert area(2, 2) == 5. What does pytest report?
Lesson
A test is a function named test_… in a file named test_….py that calls your code and asserts what must be true. pytest finds and runs them and shows what differed. Write the test first, watch it fail, then make it pass.
Demo
The tutor writes python/py050/demo/test_area_demo.py with two tests, runs python3 -m pytest demo -q, breaks area() on purpose, reads the failure report line by line, and fixes it.
Hands-on
In python/py050/s6/ create test_discount.py importing price_after from discount; write three tests: 2000 at default gives 1800, 2000 at 25 gives 1500, 999 at 50 gives 499. Run python3 -m pytest s6 -q until all pass.
Explain
What does a failing assert show you that a print does not?
Check
test_discount.py has three passing tests — checked (T3):
seat_fileseat_test6.5Lab: the budget functions10 minproven · T4
Predict
percent_used(budget=0, spent=0) — what should a careful function return?
Lesson
Three pure functions over integer cents, each tested at its boundaries: remaining(budget, spent), percent_used(budget, spent) rounded to a whole number and safe for a zero budget, and over_budget(budget, spent) as a bool. The grader will try values you did not.
Demo
The tutor sketches the three signatures in python/py050/demo/budget_demo.py, writes one boundary test for each, runs red, implements until green, then adds the zero-budget case and shows the ZeroDivisionError before guarding it.
Hands-on
Write python/py050/s6/budget.py with remaining(budget, spent) -> int, percent_used(budget, spent) -> int (round half up; 0 when budget is 0) and over_budget(budget, spent) -> bool, and s6/test_budget.py with at least four tests including the zero budget and an exactly-spent budget. All green.
Explain
Why test the zero-budget case explicitly?
Check
budget.py passes your tests and the hidden ones — proven (T4):
seat_fileseat_testcraft
Session 7 · Files and errors
Five steps on data that outlives the program. Opening a text file with with so it is always closed, reading it line by line and stripping the newline. Writing and appending, and the difference between the two modes. CSV as rows and columns through the csv module, reading with a header. try/except for the failures you expect at a boundary — a missing file, a bad number — handled where you can act and reported plainly. Live in the seat: a file read twice without with and the handle left open, a CSV with a quoted comma, FileNotFoundError caught and turned into one clear line and exit code 2.
Lab Write ledger.py: it reads ledger.csv (date, description, cents), prints each row and the total in dollars, and with an extra command-line argument appends a new row. A missing file prints one sentence and exits with code 2. Session 7 checkpoint.
5 mini steps · ~40 min
7.1Reading a file with with7 minchecked · T3
Predict
A file has two lines. for line in f: print(len(line)) with the first line "abc". What prints first?
Lesson
with open(path) as f: gives you a file that closes itself when the block ends, even on an error. Walk it with for line in f; each line keeps its newline until you strip it. Paths are relative to where you run python3.
Demo
The tutor creates python/py050/demo/notes.txt and reads it in read_demo.py three ways — f.read(), f.readlines() and the for loop — showing the trailing newlines with repr(), then the same without with and f.closed staying False.
Hands-on
Copy python/py050/samples/s7/quotes.txt to s7/; write python/py050/s7/read_quotes.py that prints each line numbered from 1 without blank lines between, then "Lines: 4".
Explain
What does with guarantee that a plain open() does not?
Check
read_quotes.py numbers four lines and counts them — checked (T3):
seat_fileseat_run7.2Writing and appending7 minchecked · T3
Predict
You open a file with mode "w" twice and write one line each time. How many lines does it have?
Lesson
open(path, "w") creates or empties the file and writes from the start; "a" appends to the end. write() adds no newline, so put "\n" in yourself. Writing in a with block flushes and closes the file for you.
Demo
In python/py050/demo/write_demo.py the tutor writes two lines with "w", shows the file, runs it again to show it still has two, switches to "a" and shows it grow to four.
Hands-on
Write python/py050/s7/log_it.py: open s7/runs.txt in append mode and write one line "run" plus a newline; then read the file back and print the number of lines. Run it three times; the third run prints "Runs: 3".
Explain
Why did the count grow with "a" but not with "w"?
Check
log_it.py appends and counts — checked (T3):
seat_fileseat_fileseat_run7.3CSV: rows and columns8 minchecked · T3
Predict
A CSV row is 2026-01-05,"Coffee, large",450. How many fields does csv.reader give?
Lesson
csv.reader turns each line into a list of fields and handles quotes for you; csv.DictReader uses the header row as keys. Open CSV files with newline="" as the documentation asks. Everything read is text until you convert it.
Demo
The tutor reads python/py050/demo/spend.csv with csv.reader, skips the header with next(), prints each row as a list, then switches to DictReader and prints row["cents"] converted with int() and summed.
Hands-on
Copy python/py050/samples/s7/ledger.csv to s7/; write python/py050/s7/sum_csv.py that reads it with DictReader, prints one "description cents" line per row, then "Total: 7775".
Explain
Why not split each line on commas yourself?
Check
sum_csv.py reads ledger.csv and totals 7775 — checked (T3):
seat_fileseat_run7.4try/except: the missing file8 minchecked · T3
Predict
open("nope.csv") when the file does not exist raises?
Lesson
try runs code that might fail; except NameOfError: runs only for that error. Catch the specific error you expect, say what went wrong in one plain line, and exit with a small non-zero code so a script can tell. Never catch everything silently.
Demo
In python/py050/demo/missing_demo.py the tutor opens a missing file, shows the traceback, wraps it in try/except FileNotFoundError, prints "Cannot find spend.csv" and calls sys.exit(2); then shows a bare except hiding a typo.
Hands-on
Write python/py050/s7/safe_read.py that takes a path from sys.argv[1], prints its line count as "Lines: N", and on a missing file prints "Cannot find <path>" and exits with code 2. Test with s7/quotes.txt and with nope.txt.
Explain
Why catch FileNotFoundError and not Exception?
Check
safe_read.py counts lines and handles a missing file with exit 2 — checked (T3):
seat_fileseat_runseat_runcraft7.5Lab: the saved ledger10 minchecked · T3
Predict
After appending a row to the CSV in mode "a", must you write the header again?
Lesson
A ledger that survives: read the CSV, list rows and total in dollars, and with three extra arguments append a row with csv.writer. A missing file is reported in one sentence with exit code 2; everything else leaves the exit code 0.
Demo
The tutor builds python/py050/demo/ledger_demo.py in three passes — list and total, append, missing-file guard — running it after each pass and showing the CSV in the editor growing by one row.
Hands-on
Write python/py050/s7/ledger.py. python3 s7/ledger.py s7/ledger.csv prints "date description cents" per row then "Total: 77.75". With three more arguments (date, description, cents) it appends that row with csv.writer and prints "Added". A missing file prints "Cannot find <path>" and exits 2.
Explain
Why print the total in dollars but store cents in the file?
Check
ledger.py lists, totals, appends and guards — checked (T3):
seat_fileseat_runseat_run
Session 8 · Capstone: Tally, the expense tracker
Five steps that build one real command-line tool from a written spec. Reading the spec and sketching the functions before typing. argparse for arguments and help text, and an exit-code contract: 0 all good, 2 cannot read the file, 3 some rows rejected. Loading rows and rejecting the bad ones with a reason, without stopping. Totals and a per-category report aligned to the cent with Decimal. Then the whole thing on inputs you have not seen: the grader runs Tally with its own files and its own tests. Live in the seat: the spec turned into four function signatures, --help generated for free, a bad row counted but not crashing, and the report checked against the published expected output.
Lab Finish tally.py: it reads an expenses CSV, prints the total, a category report and a row summary, exits 0, 2 or 3 as specified, and passes your tests and the hidden ones on files you did not write. Course placement step.
5 mini steps · ~43 min
8.1Reading the spec7 minchecked · T3
Predict
Which function should not print anything: load_rows, totals_by_category or main?
Lesson
Tally reads expenses.csv (date, category, description, amount) and prints Total, a By category block sorted by name, and "Rows: N ok, M rejected". Before coding, name the pieces: load_rows(path), totals_by_category(rows), format_report(...) and main(argv).
Demo
The tutor reads the spec in python/py050/samples/s8/SPEC.md aloud, writes the four signatures with docstrings and pass bodies in demo/tally_skeleton.py, and marks which ones the tests will call.
Hands-on
Create python/py050/s8/tally.py with the four function signatures — load_rows(path), totals_by_category(rows), format_report(total, by_cat, ok, rejected) and main(argv) — each with a one-line docstring and a pass body, plus the if __name__ == "__main__": guard calling main(sys.argv[1:]).
Explain
Why write the signatures before any logic?
Check
tally.py has the four signatures and the main guard — checked (T3):
seat_file8.2argparse and the exit-code contract8 minchecked · T3
Predict
A caller runs tally.py with no arguments. What should happen under the contract?
Lesson
argparse declares the arguments once and gives you --help, error messages and exit code 2 for free. Your contract: 0 when every row loaded, 2 when the file cannot be read, 3 when some rows were rejected but the report still printed.
Demo
In demo/args_demo.py the tutor adds an ArgumentParser with a positional csv path and an optional --category filter, runs --help, runs with a missing path and sys.exit(2), and shows the shell reading the exit code with echo $?.
Hands-on
In s8/tally.py make main(argv) parse one positional argument "path" with argparse; on FileNotFoundError print "Cannot read <path>" and return 2; otherwise return 0 for now. Return the code from main and pass it to sys.exit in the guard. Try --help and a missing file.
Explain
Why return the exit code from main instead of calling sys.exit inside it?
Check
tally.py parses its argument and exits 2 on a missing file — checked (T3):
seat_fileseat_runseat_run8.3Loading and rejecting rows9 minchecked · T3
Predict
A row has amount "abc". What should load_rows do?
Lesson
load_rows returns (rows, rejected): each good row a dict with the amount as Decimal; a row with fewer than four fields or an amount Decimal cannot parse adds one to rejected. Catch decimal.InvalidOperation, not Exception.
Demo
The tutor implements load_rows in demo/load_demo.py over samples/s8/sample.csv, prints the row count and rejected count (5 and 1), then adds a row with three fields to show the second rejection path.
Hands-on
Implement load_rows(path) in s8/tally.py using csv.DictReader and Decimal, returning (rows, rejected). Write s8/test_tally.py with a test that loads samples/s8/sample.csv and asserts 5 rows and 1 rejected, and one that a row amount is Decimal("12.50").
Explain
Why return the rejected count instead of printing it inside load_rows?
Check
load_rows returns 5 rows and 1 rejection for sample.csv — checked (T3):
seat_fileseat_test8.4Totals and the category report9 minchecked · T3
Predict
Categories food, transport and home. In what order does the report list them?
Lesson
totals_by_category folds the rows into a dictionary of Decimal totals; format_report builds the text: "Total: 223.45", "By category:", one " name total" line per category sorted by name with widths <12 and >10.2f, then "Rows: 5 ok, 1 rejected".
Demo
The tutor writes totals_by_category and format_report in demo/report_demo.py, prints the report for sample.csv, and compares it line by line with samples/s8/expected_sample.txt in a split editor.
Hands-on
Implement totals_by_category(rows) and format_report(total, by_cat, ok, rejected) in s8/tally.py; wire main to print the report and return 3 when rejected > 0 else 0. python3 s8/tally.py samples/s8/sample.csv must print exactly samples/s8/expected_sample.txt and exit 3.
Explain
Why keep format_report separate from main?
Check
tally.py prints the published report for sample.csv — checked (T3):
seat_run8.5Lab: Tally on inputs you did not see10 minproven · T4
Predict
A file with a header and no data rows. What are the total and the exit code?
Lesson
The capstone check: the grader runs your tally.py on files you did not write — a clean file, an empty one, one with every row bad — and runs hidden tests against load_rows and totals_by_category. Your own tests should already cover those shapes.
Demo
The tutor shows the three published cases in samples/s8/ (clean.csv exit 0, empty.csv exit 0 with Total: 0.00, allbad.csv exit 3), runs the reference on each, and explains that two more files stay hidden.
Hands-on
Make s8/tally.py pass the three published cases and add tests for an empty file and an all-rejected file to s8/test_tally.py. Then ask the tutor to check: the grader runs the hidden files and hidden tests. Fix what it reports.
Explain
Why does passing on your own sample prove less than passing on hidden ones?
Check
Tally passes the published and hidden files and tests — proven (T4):
seat_runseat_testseat_filecraft
Outcome
By the end of the course the tutor's checks will have verified: a working program for every hands-on step; eight session labs that each grow the last — the greeting, the receipt, the tip calculator, the quiz, the shopping list, the budget functions, the saved ledger; and the Tally capstone, a command-line expense tracker whose totals, category report and exit codes match the published expected values on inputs you did not see.
Certificate
Agile System Software Certificate — Python from zero (PY050) attests that you can write, run, test and debug a small command-line Python program that reads and writes files, with functions, collections and an exit-code contract, verified by a machine from your own seat.
To earn it:
- Pass the lab check of every session (steps 1.5, 2.5, 3.5, 4.5, 5.5, 6.5 and 7.5) on your own code.
- Pass the Tally capstone check (8.5) including the hidden input files the grader supplies.
- Score 80% or better on the 20-question reading quiz.
Gates (step · tier required):
Plus a 20-question reading quiz at 80%, retake after 48 h. Certificates are issued once the seat check runner is live; until then every step records what the engine saw.
Environment
A VS Code seat in your browser with Python 3.13, pytest and basedpyright, ready and offline; nothing to install. Your work lives in python/py050; the tutor's demo files live apart from yours and the tutor never edits your hands-on file while you are working on it.
Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick PY050 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.