Lessons
Unit 1: Foundations
Lesson 1: Setup and Your First Program
Python is the world's most popular first programming language β readable, versatile, and running everything from Instagram's backend to NASA's data analysis. To start, install Python 3 from python.org (or use an online editor like Replit β no install needed). Verify with python --version in your terminal. A good code editor (VS Code is free and excellent) with syntax highlighting will catch mistakes your eyes miss.
A program is a sequence of instructions the computer executes top to bottom. Python's print() function displays output. Your first program is tradition:
print("Hello, world!")
print("My name is Ada.")
print(2 + 3 * 4)
Run it and you'll see three lines: the two messages and 14 (Python follows the same order of operations as math). Text in quotes is a string; # starts a comment the computer ignores β use comments to explain why, not what.
Errors are normal β even professionals write bugs daily. Read error messages bottom-up: the last line names the error type and line number. A SyntaxError means Python couldn't parse your code (often a missing quote or parenthesis); a NameError means you used a name that doesn't exist (check spelling and capitalization β Print is not print). Debugging is detective work: reproduce, read, hypothesize, test.
Worked example — a program that introduces you:
- Open a new file named intro.py in your editor.
- Type:
print("Hi! I'm learning Python.")
print("Today is a great day to code.")
# The line below does math
print("7 * 6 =", 7 * 6)
- Save, then run:
python intro.py.
- Expected output: the two messages, then
7 * 6 = 42 (print accepts multiple comma-separated items).
- Experiment: change the math, add your name, and re-run. Breaking and fixing things is how learning happens.
Try it yourself: Write a program that prints your name, your grade, and the result of (10 + 5) * 2. Then intentionally delete a closing quote, run it, and read the SyntaxError β fix it.
Lesson 2: Variables, Types, and Control Flow
A variable is a named box holding a value: score = 0 creates the name score pointing at 0. Python's core types: int (whole numbers), float (decimals), str (text), bool (True/False). Variables can be reassigned and even change type β Python is dynamically typed. F-strings embed values in text elegantly: f"Score: {score}".
input() reads user text (always as a string β convert with int() or float() for math):
name = input("What is your name? ")
age = int(input("How old are you? "))
print(f"{name}, next year you'll be {age + 1}.")
Conditionals branch on decisions with if/elif/else. Indentation (4 spaces) β not braces β defines blocks, which forces readable code. Comparison operators (==, !=, <, >, <=, >=) and logic operators (and, or, not) build conditions. Beware = (assignment) vs == (comparison) β the classic beginner bug.
Loops repeat. while loops until a condition fails (great when the count is unknown); for loops iterate a known sequence, usually range(): range(5) gives 0β4, range(1, 11) gives 1β10. break exits a loop early; continue skips to the next iteration. An infinite while True loop needs a break β or Ctrl+C to escape.
Worked example — a number-guessing game:
secret = 7
guess = int(input("Guess a number 1-10: "))
while guess != secret:
if guess < secret:
print("Too low!")
else:
print("Too high!")
guess = int(input("Try again: "))
print("Correct! You win.")
- Trace: guess 3 β 'Too low!' β loop repeats; guess 9 β 'Too high!'; guess 7 β loop exits.
- Key detail:
guess is re-read inside the loop β without that line, the loop never ends.
- Improvement ideas: count attempts, or use
random.randint(1, 10) for a new secret each game.
Try it yourself: Write a program that asks for a test score and prints A/B/C/D/F (90+, 80+, 70+, 60+, below). Then write a for loop printing the 7-times table from 7Γ1 to 7Γ12.
Lesson 3: Strings and String Methods
A string is text in quotes: "hello". Strings support concatenation ("a" + "b" → "ab"), repetition ("ha" * 3 → "hahaha"), and indexing: in s = "python", s[0] is "p" and s[-1] is "n" (negative indices count from the end). A slice takes a range: s[1:4] is "yth" (up to but not including index 4). Strings are immutable: methods return new strings rather than changing the original.
Methods are functions attached to a value. "Hello".lower() gives "hello"; " hi ".strip() removes surrounding spaces; "a,b,c".split(",") gives ["a", "b", "c"]; "-".join(["a", "b"]) gives "a-b"; "hello world".replace("world", "Python") gives "hello Python". f-strings embed values: f"Hi, {name}! You are {age}.". The len() function counts characters, and in tests membership: "ell" in "hello" is True.
Strings drive real programs: cleaning user input (.strip().lower()), parsing CSV lines with split(","), and building messages with f-strings. A common bug is comparing strings with different cases — normalize case before comparing.
Worked example — normalizing and parsing " Alice, 95 , Math ":
s = " Alice, 95 , Math ". s.split(",") gives [" Alice", " 95 ", " Math "].
- Strip each part:
[p.strip() for p in s.split(",")] gives ["Alice", "95", "Math"].
- Convert the score:
int("95") is 95. Build a message: f"{name} scored {score} in {subject}."
Try it yourself: Given email = "User@Example.COM", write code to normalize it to lowercase without spaces. What does "banana"[2:5] give? Answers: email.strip().lower() → "user@example.com"; "banana"[2:5] is "nan".
Download Worksheets
Print these worksheets for offline practice and homework. Each PDF includes an answer key on the last pages (for teacher/parent use).
Key vocabulary: String — a sequence of characters; method — a function attached to a value; immutable — cannot be changed in place; f-string — a string with embedded expressions.
Lesson 4: Functions
A function is a named, reusable chunk of code: define once with def, call many times. Functions take parameters (inputs) and return values (outputs):
def area_circle(radius):
pi = 3.14159
return pi * radius ** 2
print(area_circle(5)) # 78.53975
print(area_circle(10)) # 314.159
Without return, a function gives back None β printing inside is not returning. Parameters are placeholders; arguments are the actual values passed. Default parameters (def greet(name="friend")) make arguments optional.
Scope rules where variables live: variables created inside a function are local β invisible outside. This isolation is a feature, preventing name collisions across a big program. Docstrings (triple-quoted descriptions under def) document purpose, and good function names (calculate_tax, not ct) are documentation too. One function, one job β if you describe it with 'and', split it.
Functions compose into programs the way sentences compose into essays. Python's standard library is a giant toolbox: import math gives math.sqrt(); import random gives random.choice(). Reading documentation (docs.python.org) is a core programmer skill β nobody memorizes everything.
Worked example — a tip calculator built from functions:
def calculate_tip(bill, percent=15):
"""Return the tip amount for a bill."""
return bill * percent / 100
def split_bill(bill, percent, people):
total = bill + calculate_tip(bill, percent)
return total / people
bill = float(input("Bill amount? "))
people = int(input("How many people? "))
print(f"Each pays: ${split_bill(bill, 18, people):.2f}")
- Note
:.2f formats to 2 decimal places β money formatting.
split_bill reuses calculate_tip: functions calling functions.
- Test: bill 100, 4 people, 18% β tip 18, total 118, each pays $29.50.
Try it yourself: Write is_even(n) returning True/False, and celsius_to_fahrenheit(c). Then write grade_letter(score) and use it to print letter grades for [95, 82, 71, 58].
Unit 2: Data, Loops, and Files
Lesson 5: Lists, Dictionaries, and Loops
A list holds an ordered collection: scores = [88, 92, 75, 100]. Index from 0: scores[0] is 88; scores[-1] is the last item. Lists are mutable β append(), remove(), pop(), and assignment (scores[1] = 95) change them in place. len() counts items; sum(), max(), min() summarize numbers. Slicing grabs ranges: scores[1:3] is [92, 75].
A dictionary maps keys to values: capitals = {"France": "Paris", "Japan": "Tokyo"}. Look up with capitals["France"]; add with capitals["Peru"] = "Lima". Dictionaries power counting patterns:
votes = ["a", "b", "a", "c", "a"]
counts = {}
for v in votes:
counts[v] = counts.get(v, 0) + 1
print(counts) # {'a': 3, 'b': 1, 'c': 1}
Tuples ((3, 4)) are immutable lists β perfect for fixed data like coordinates. Sets ({1, 2, 3}) hold unique items and support membership tests and math-like operations.
Loop idioms: iterate directly (for s in scores), with index (for i, s in enumerate(scores)), or over dict items (for k, v in capitals.items()). List comprehensions build lists declaratively: squares = [x**2 for x in range(10)]. Nested loops handle grids and tables β and nested data (a list of dictionaries) models real records, like a class roster.
Worked example — analyzing quiz scores:
scores = [88, 92, 75, 100, 67, 95, 81]
average = sum(scores) / len(scores)
passed = [s for s in scores if s >= 70]
print(f"Average: {average:.1f}")
print(f"Highest: {max(scores)}, Lowest: {min(scores)}")
print(f"Passed: {len(passed)}/{len(scores)}")
grades = {}
for s in scores:
letter = "A" if s >= 90 else "B" if s >= 80 else "C" if s >= 70 else "F"
grades[letter] = grades.get(letter, 0) + 1
print(grades)
- Output: average 85.4, highest 100, lowest 67, passed 6/7, grades {'B': 2, 'A': 2, 'C': 1, 'F': 1}... (verify: 88 B, 92 A, 75 C, 100 A, 67 F, 95 A, 81 B β A:3, B:2, C:1, F:1).
- The chained conditional expression assigns letter grades in one line.
Try it yourself: Given temps = [72, 68, 75, 79, 71], print the average and how many days were above 73. Build a dictionary mapping each of your friends' names to their favorite color, then print each pair.
Lesson 6: File Handling and Exceptions
Programs often outlive a single run by storing data in files. The safe pattern uses with: with open("notes.txt", "w") as f: f.write("hello") writes a file and closes it automatically, even if an error occurs. Modes: "r" read, "w" write (erases existing content!), "a" append. f.read() returns the whole file as one string; f.readlines() returns a list of lines. To save structured data, the json module converts Python lists and dictionaries to text and back: json.dump(scores, f) and json.load(f).
Things go wrong: a missing file, a bad conversion, a division by zero. An exception is Python's way of reporting the failure, and try/except lets you handle it gracefully instead of crashing: try: n = int(input("Age: ")) except ValueError: print("Please enter a number."). Catch specific exceptions, not a bare except, so real bugs are not silently hidden. finally runs cleanup code either way.
Combine the ideas: a quiz-game high-score file that loads on startup (creating an empty one if missing), updates after each game, and saves with json.dump. That is the skeleton of every app that remembers anything.
Worked example — safely reading a scores file:
try: with open("scores.json") as f: scores = json.load(f) except FileNotFoundError: scores = []
- If the file is missing, start with an empty list instead of crashing.
- After the game:
scores.append(new_score), then with open("scores.json", "w") as f: json.dump(scores, f).
Try it yourself: Write code that appends the line "done" to log.txt without erasing it. Which exception type appears for int("abc")? Answers: with open("log.txt", "a") as f: f.write("done\n"); ValueError.
Key vocabulary: Exception — a runtime error object; try/except — handling an exception gracefully; JSON — a text format for structured data; with statement — automatic resource cleanup.
Lesson 7: Debugging Strategies and Testing
Debugging is not guessing — it is the scientific method applied to code. Reproduce the bug reliably first. Read the traceback bottom-up: the last line names the exception and the line that raised it. Isolate by bisecting: comment out half the code or test the suspicious function alone with known inputs. Print the values at the point of failure: is the variable what you expected? Most bugs are wrong assumptions about values, not mysterious language behavior.
Common Python traps: off-by-one errors (range(5) gives 0–4, not 1–5); comparing with = instead of ==; mutating a list while looping over it; using a variable before assignment; and the classic if x == 1 or 2, which is always True because it means (x == 1) or (2). Write if x in (1, 2) instead.
Testing prevents bugs from coming back. A unit test checks one function against known inputs and outputs: assert add(2, 3) == 5. Test edge cases: empty input, zero, negative numbers, the largest expected value. Professional teams run hundreds of such tests automatically before every release.
Worked example — debugging a function that returns the wrong average:
- Buggy code:
def avg(nums): return sum(nums) / len(nums) - 1. For [10, 20], it returns 14 instead of 15.
- Print:
sum(nums) is 30, len(nums) is 2, so sum/len is 15 — then - 1 is the culprit.
- Fix:
return sum(nums) / len(nums). Add tests: assert avg([10, 20]) == 15, assert avg([5]) == 5.
Try it yourself: What does if x == "yes" or "y": do when x is "no"? Why? Answers: It is True (because "y" is truthy), so the branch runs anyway. Write if x in ("yes", "y"):.
Key vocabulary: Traceback — Python's error report; unit test — a check of one function against expected output; edge case — an extreme or boundary input; assert — a statement that fails loudly if a condition is false.
Lesson 8: Algorithms — Searching and Sorting
An algorithm is a precise procedure for solving a problem. Linear search checks each item in turn: worst case, it scans the whole list (O(n)). Binary search works on a sorted list: compare the middle item, discard the half that cannot contain the target, and repeat. Finding a name in a sorted list of a million takes at most 20 comparisons by binary search, versus up to a million by linear search. The catch: the list must be sorted first.
Sorting arranges items in order. Bubble sort repeatedly swaps adjacent out-of-order pairs — simple but slow (O(n²)). Python's built-in sorted() uses Timsort, a fast hybrid algorithm (O(n log n)), and is almost always the right choice: sorted([3, 1, 2]) gives [1, 2, 3], and sorted(names, key=len) sorts by length. Understanding bubble sort matters because it teaches how to reason about correctness and speed — the skills behind every other algorithm.
Big-O notation describes how running time grows with input size: O(1) constant, O(log n) halving, O(n) scanning once, O(n log n) efficient sorting, O(n²) nested loops. An O(n²) algorithm on 10,000 items does 100 million operations — the difference between instant and unusable.
Worked example — binary search for 7 in [1, 3, 5, 7, 9, 11, 13]:
- Middle (index 3) is 7. Found in one step!
- For 11: middle is 7 < 11, so search the right half [9, 11, 13]. Middle is 11. Found in two steps.
- For 4: middle 7 > 4, search left [1, 3, 5]. Middle 3 < 4, search [5]. 5 ≠ 4 — not present.
Try it yourself: How many comparisons (worst case) does binary search need on 1,024 sorted items? Sort [5, 2, 9, 1] by hand with bubble sort; how many passes? Answers: log₂(1024) = 10. Bubble sort: pass 1 → [2, 5, 1, 9], pass 2 → [2, 1, 5, 9], pass 3 → [1, 2, 5, 9].
Key vocabulary: Algorithm — a precise problem-solving procedure; binary search — halving search on sorted data; Big-O — how runtime grows with input size.
Unit 3: Projects and Object-Oriented Programming
Lesson 9: Mini-Project β Quiz Game
Time to build something real: a complete quiz game combining everything β data structures for questions, loops for gameplay, conditionals for scoring, functions for organization, and file-free randomness via random.shuffle. Professional programs separate data (the questions), logic (asking, checking), and presentation (messages, scores).
Design first: each question is a dictionary with question, options (list), and answer (index). ask_question() handles one question and returns True/False; run_quiz() shuffles, loops, tallies, and reports a grade. Shuffling the options too (while tracking the correct one) prevents memorizing positions β a nice touch showing real software thinking.
Run it, then extend it: add a timer with the time module, load questions from a file, track high scores across runs, or add lifelines. Every feature you add practices the same cycle professionals use: design, code, test, refactor. The full program below is yours to type, run, and own.
Worked example — the complete quiz game β type it, run it, extend it:
import random
QUESTIONS = [
{"q": "What is 7 * 8?", "options": ["54", "56", "63", "48"], "answer": 1},
{"q": "Which planet is known as the Red Planet?",
"options": ["Venus", "Jupiter", "Mars", "Saturn"], "answer": 2},
{"q": "What does 'CPU' stand for?",
"options": ["Central Processing Unit", "Computer Personal Unit",
"Central Program Utility", "Core Processing Unit"], "answer": 0},
{"q": "Which language is this course about?",
"options": ["Java", "Python", "C++", "Ruby"], "answer": 1},
{"q": "What is the capital of Japan?",
"options": ["Seoul", "Beijing", "Osaka", "Tokyo"], "answer": 3},
]
def ask_question(item):
"""Ask one question; return True if answered correctly."""
print("\n" + item["q"])
order = list(range(len(item["options"])))
random.shuffle(order)
for i, opt_idx in enumerate(order):
print(f" {i + 1}. {item['options'][opt_idx]}")
try:
choice = int(input("Your answer (number): ")) - 1
except ValueError:
print("Not a number β counted as wrong.")
return False
if 0 <= choice < len(order) and order[choice] == item["answer"]:
print("Correct!")
return True
correct = item["options"][item["answer"]]
print(f"Wrong. The answer was: {correct}")
return False
def grade_letter(pct):
if pct >= 90: return "A"
if pct >= 80: return "B"
if pct >= 70: return "C"
if pct >= 60: return "D"
return "F"
def run_quiz():
print("=== Welcome to the Quiz Game! ===")
name = input("Enter your name: ")
questions = QUESTIONS[:]
random.shuffle(questions)
score = sum(1 for q in questions if ask_question(q))
pct = 100 * score / len(questions)
print(f"\n{name}, you scored {score}/{len(questions)} ({pct:.0f}%) β Grade: {grade_letter(pct)}")
if pct == 100:
print("Perfect score! Outstanding.")
if __name__ == "__main__":
run_quiz()
- How it works:
QUESTIONS is a list of dicts; ask_question shuffles option order so the correct answer moves; run_quiz shuffles question order, sums correct answers with a generator expression, and grades.
- The
if __name__ == "__main__": guard is professional practice β it runs the game only when the file is executed directly, not when imported.
- Extensions to try: add 5 questions of your own, add a 10-second timer per question with
time, or save high scores to a text file.
Try it yourself: Type the full game and run it. Then add: (1) three questions you write yourself, (2) a 'streak' bonus message for 3 correct in a row, (3) a final percentage with one decimal place.
Lesson 10: Object-Oriented Programming — Classes and Objects
Object-oriented programming (OOP) bundles data and the functions that work on it into objects. A class is the blueprint; an object is an instance built from it. class Dog: def __init__(self, name): self.name = name; def bark(self): return f"{self.name} says woof!". Then d = Dog("Rex"); d.bark() returns "Rex says woof!". __init__ is the constructor that sets up each new object; self refers to the object itself.
OOP shines when a program has many similar things: a quiz app with many Question objects, a game with many Player objects, a school system with Student and Course objects. Each object keeps its own state (self.score) and behavior (add_points()), so code stays organized as programs grow.
Inheritance lets a class reuse another: class Cat(Pet): ... gets all of Pet's methods and can override or extend them. A bank account class with deposit and withdraw methods, plus a SavingsAccount subclass that adds interest, is the textbook example.
Worked example — a BankAccount class:
class BankAccount: def __init__(self): self.balance = 0; def deposit(self, amount): self.balance += amount; def withdraw(self, amount): if amount <= self.balance: self.balance -= amount; return True; return False
acct = BankAccount(); acct.deposit(100); acct.withdraw(30) → balance 70.
acct.withdraw(1000) returns False and leaves the balance unchanged — the object guards its own rules.
Try it yourself: Write a Rectangle class with __init__(width, height) and an area() method. What does Rectangle(3, 4).area() return? Answers: 12.
Key vocabulary: Class / object — blueprint / instance; __init__ — the constructor; method — a function defined on a class; inheritance — a class reusing another's behavior.
Lesson 11: Capstone — Build a Study Planner App
Time to combine everything: build a study planner that stores tasks, sorts them by due date, saves to a file, and runs from the terminal. Plan the program as pieces before coding: (1) a Task class with subject, title, due date, and done flag; (2) functions to add, list, mark done, and delete tasks; (3) file save/load with json; (4) a menu loop that keeps running until the user quits. Planning on paper first is what separates working programs from tangled ones.
Structure: keep data in a list of Task objects. Listing sorts by due date with sorted(tasks, key=lambda t: t.due). Saving converts each task to a dictionary (t.__dict__) and dumps the list; loading rebuilds objects from dictionaries. Wrap the menu's number parsing in try/except ValueError so a typo cannot crash the app.
Stretch goals: filter tasks by subject; show overdue tasks in a different color (ANSI codes or simple markers like [OVERDUE]); add a weekly review that counts completed tasks. When it works, you have built a real, useful application — the same architecture, at larger scale, behind every to-do app.
Worked example — the main loop skeleton:
tasks = load_tasks() — load saved data (or [] if none).
while True: print menu; choice = input("> "); if choice == "1": add_task(tasks) elif choice == "2": list_tasks(tasks) elif choice == "q": save_tasks(tasks); break else: print("Unknown choice.")
- Each menu option is one function — easy to test one at a time.
Try it yourself: Add a “mark done” feature: list tasks with numbers, read the user's number, set that task's done to True, and save. What should happen if the user enters a number with no matching task? Answers: Print a friendly error (guard with if 1 <= n <= len(tasks)) instead of crashing.
Key vocabulary: Capstone — a project combining everything learned; architecture — how a program's parts fit together; persistence — saving data between runs.
Lesson 12: Modules, Libraries, and Virtual Environments
A module is a Python file full of reusable code; a library (or package) is a collection of modules. Python ships with a large standard library β "batteries included" β so you rarely start from scratch. import math gives you math.sqrt() and math.pi; import datetime gives dates and times; import random gives shuffling and random choices. You can import a whole module (import math), specific names (from math import sqrt), or give it a nickname (import datetime as dt). Writing your own module is just saving functions in a .py file and importing it from another file in the same folder.
Beyond the standard library lies PyPI, the Python Package Index, with hundreds of thousands of third-party packages β requests for the web, matplotlib for charts, pygame for games. Install with pip install requests. Because different projects may need different versions of the same package, professionals isolate each project in a virtual environment: python -m venv venv creates one, and activating it keeps that project's packages separate from every other project's. A requirements.txt file (made with pip freeze > requirements.txt) records exactly which packages and versions a project needs, so anyone can reproduce your setup.
Real-world application: every professional Python project β from Instagram's backend to a data-science notebook β runs inside a virtual environment with a pinned requirements file, so "it works on my machine" becomes "it works on every machine."
Worked example — using the standard library:
import math then math.sqrt(144) returns 12.0; math.ceil(4.2) returns 5.
import datetime then datetime.date.today() returns today's date, e.g. datetime.date(2026, 10, 14).
from collections import Counter then Counter("banana") returns Counter({'a': 3, 'n': 2, 'b': 1}) β a one-line letter count.
Worked example — your own module:
- Save
greetings.py containing def hello(name): return f"Hello, {name}!".
- In another file in the same folder:
import greetings then print(greetings.hello("Ada")) prints Hello, Ada!.
- If both files are not in the same folder, Python raises
ModuleNotFoundError β it only searches the script's folder and installed packages.
Try it yourself: Use random to print a random number from 1 to 100, and use datetime to print how many days remain until your next birthday. Then create a module shapes.py with area_square(side) and import it. Answers: random.randint(1, 100); subtract datetime.date.today() from the birthday date and read .days.
Key vocabulary: Module — a reusable .py file; library/package — a collection of modules; PyPI — the public package index; pip — the package installer; virtual environment — an isolated per-project package setup; requirements.txt — a pinned list of a project's packages.
Unit 4: Advanced Python
Lesson 13: Working with Data — CSV, JSON, and APIs
Real programs rarely work with data typed by hand β they read files and fetch data from the internet. CSV (comma-separated values) is the simplest table format: one row per line, columns separated by commas. Python's csv module reads it safely (handling quoted commas correctly, which a bare split(",") does not): csv.DictReader(f) turns each row into a dictionary keyed by the header row. JSON (JavaScript Object Notation) is the web's data format β it looks like Python lists and dictionaries, and json.load() / json.dump() convert between JSON text and Python objects.
An API (Application Programming Interface) is a URL that returns data instead of a web page. For example, a weather API might return {"city": "Boston", "temp_c": 18} as JSON. With the requests library (pip install requests), requests.get(url).json() fetches and parses that data in two lines. Always check the response status: 200 means success; 404 means not found. Many APIs need a free key and limit how many requests you can make β read their documentation first.
Real-world application: weather apps, stock tickers, sports scores, and map services all work by fetching JSON from APIs and displaying it β the same pattern you are learning here, at planetary scale.
Worked example — reading a CSV of quiz scores:
- File
scores.csv:name,score
Ada,95
Ben,82
import csv
with open("scores.csv") as f:
for row in csv.DictReader(f):
print(row["name"], int(row["score"]))
prints Ada 95 and Ben 82.
DictReader uses the first row as keys, so each row is a dictionary β no manual splitting needed.
Worked example — parsing JSON from an API-style string:
import json
data = json.loads('{"city": "Boston", "temp_c": 18}')
print(f"{data['city']}: {data['temp_c']}Β°C") # Boston: 18Β°C
json.loads parses a string; json.load(f) parses a file. The result is an ordinary Python dictionary.
- With
requests: data = requests.get(url).json() does the fetching and parsing in one step.
Try it yourself: Write a CSV file with three friends' names and favorite numbers, read it with csv.DictReader, and print the average favorite number. Then use json.dumps to save a dictionary of your scores to a file. Answers: Convert each row's number with int() before averaging; json.dump(scores, open("scores.json", "w")) saves it.
Key vocabulary: CSV — comma-separated table format; JSON — the web's text data format; API — a URL that returns data; DictReader — a CSV reader yielding dictionaries; status code — a number like 200 (success) or 404 (not found) from a web request.
Lesson 14: Error Handling in Depth and Logging
Lesson 6 introduced try/except. Now go deeper: a full try block has four parts. try holds the risky code; except handles specific errors; else runs only if no error occurred (use it for the "success path" so the try stays small); finally always runs β ideal for closing files or network connections. Catch the most specific exception you can: except FileNotFoundError and except ValueError separately, rather than a bare except that hides every bug, including typos.
You can also raise your own exceptions when inputs break your function's contract: if age < 0: raise ValueError("Age cannot be negative"). For reusable checks, define a custom exception by subclassing Exception. And when something goes wrong in a long-running program, print() debugging is not enough β use the logging module: logging.warning("Low disk") writes timestamped messages with severity levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) that you can send to a file instead of the screen.
Real-world application: web servers log every error with timestamps so engineers can diagnose outages at 3 a.m. without reproducing them β logging is how software explains itself.
Worked example — the full try/except/else/finally pattern:
try:
n = int(input("Enter a number: "))
except ValueError:
print("That was not a number.")
else:
print(f"Double: {n * 2}")
finally:
print("Done.")
- Input
5 prints Double: 10 then Done. Input abc prints the error message then Done. β finally always runs.
- Rule: keep the
try block as small as possible so you know exactly which line can fail.
Worked example — raising and logging:
import logging
logging.basicConfig(level=logging.INFO)
def divide(a, b):
if b == 0:
logging.error("divide called with b=0")
raise ValueError("Cannot divide by zero")
return a / b
divide(10, 0) logs an ERROR line with a timestamp, then raises ValueError to the caller.
- Callers decide what to do: crash loudly, catch it, or retry β the function reports; it does not silently return a wrong answer.
Try it yourself: Write a function that reads an integer from the user, retrying up to 3 times on ValueError before giving up. Which clause runs whether or not an error occurs? Answers: Use a for loop with try/except ValueError: continue and a return on success; finally always runs.
Key vocabulary: else (in try) — runs only on success; finally — always runs; raise — deliberately trigger an exception; custom exception — your own Exception subclass; logging — timestamped, leveled diagnostic messages.
Lesson 15: Recursion and Functional Programming
Recursion is a function calling itself to solve smaller copies of a problem. Every recursive function needs two parts: a base case that stops the recursion, and a recursive step that moves toward it. The classic example: factorial(n) = n * factorial(n - 1) with base case factorial(0) = 1. Trace factorial(3): 3 * factorial(2) β 3 * (2 * factorial(1)) β 3 * (2 * (1 * factorial(0))) β 3 * 2 * 1 * 1 = 6. Each call waits on a stack of frames until the base case unwinds them. Python limits recursion depth (default 1000), so deep recursion raises RecursionError β iteration is often safer for very long sequences.
Functional programming treats functions as values: you can pass them to other functions. A lambda is a tiny anonymous function: lambda x: x ** 2. map() applies a function to every item (list(map(lambda x: x * 2, [1, 2, 3])) is [2, 4, 6]); filter() keeps items that pass a test (list(filter(lambda x: x % 2 == 0, [1, 2, 3, 4])) is [2, 4]); and sorted(names, key=len) sorts by a function. In modern Python, list comprehensions usually replace map/filter, but recognizing them matters because they appear throughout real code and other languages.
Real-world application: file-system search, JSON tree traversal, and directory-size tools are naturally recursive; data pipelines in libraries like pandas chain map-style transformations over millions of rows.
Worked example — recursive countdown and Fibonacci:
def countdown(n):
if n <= 0: # base case
print("Blast off!")
else:
print(n)
countdown(n - 1) # recursive step
countdown(3) prints 3, 2, 1, "Blast off!".
def fib(n): return n if n < 2 else fib(n-1) + fib(n-2) β correct but slow: fib(30) makes over a million calls because it recomputes the same values.
- Fix with memoization (Lesson: Data Structures): cache results in a dictionary and the same function becomes nearly instant.
Worked example — sorting with key functions:
words = ["apple", "fig", "banana"]; sorted(words, key=len) gives ["fig", "apple", "banana"] β sorted by length.
sorted(words, key=lambda w: w[-1]) sorts by last letter: ["banana", "apple", "fig"] (a, e, g).
- The
key function is called once per item; Python sorts by the returned keys. This is how spreadsheets sort "by column."
Try it yourself: Write recursive sum_list(nums) (base case: empty list β 0). What does list(map(lambda x: x ** 2, [2, 3])) give? Answers: return 0 if not nums else nums[0] + sum_list(nums[1:]); [4, 9].
Key vocabulary: Recursion — a function calling itself; base case — the stopping condition; recursive step — progress toward the base case; lambda — a tiny anonymous function; map / filter — apply-a-function / keep-matching-items; memoization — caching results to avoid recomputation.
Lesson 16: Object-Oriented Programming II — Inheritance, Polymorphism, and Dunder Methods
Lesson 10 built single classes. Real OOP adds three power tools. Inheritance lets a class reuse another: class Dog(Pet) inherits Pet's methods and can add or override its own. The child calls the parent's setup with super().__init__(name). Polymorphism ("many shapes") means different classes can share a method name and be used interchangeably: a Dog and a Cat both have speak(), so a loop can call animal.speak() on either without checking its type. Composition ("has-a" instead of "is-a") builds objects from other objects: a School has a list of Student objects.
Dunder methods (double-underscore, like __init__) let your objects behave like built-ins. Define __str__ to control print(), __len__ for len(), __eq__ for ==, and __add__ for +. A Vector class with __add__ can be added with v1 + v2 just like numbers. Encapsulation means an object guards its own data: methods like withdraw() enforce rules so outside code cannot put the object in an invalid state.
Real-world application: game engines model every character, item, and level as objects in inheritance hierarchies; web frameworks model database rows as objects with dunder-powered behavior.
Worked example — inheritance with super():
class Pet:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Dog(Pet):
def __init__(self, name, breed):
super().__init__(name) # reuse Pet's setup
self.breed = breed
def speak(self): # override
return f"{self.name} says woof!"
class Cat(Pet):
def speak(self):
return f"{self.name} says meow!"
for pet in [Dog("Rex", "Lab"), Cat("Mimi")]: print(pet.speak()) prints Rex says woof! then Mimi says meow! β polymorphism: one loop, two behaviors.
isinstance(Dog("Rex", "Lab"), Pet) is True β a Dog is a Pet.
Worked example — dunder methods make objects Pythonic:
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
def __str__(self):
return f"Playlist with {len(self)} songs"
p = Playlist(["a", "b"]); len(p) is 2; print(p) shows Playlist with 2 songs.
- Without
__str__, print(p) would show an ugly <__main__.Playlist object ...>.
Try it yourself: Create class SavingsAccount(BankAccount) (from Lesson 10) adding add_interest(rate). Then add __str__ to BankAccount showing the balance. What does super().__init__() do? Answers: It calls the parent class's __init__ so inherited setup still runs.
Key vocabulary: Inheritance — a class reusing a parent's behavior; super() — access to the parent class; polymorphism — different classes sharing a method name; composition — building objects from other objects; dunder method — a __name__ method giving built-in behavior; encapsulation — guarding an object's data behind methods.
Unit 5: Professional Python
Lesson 17: Testing, Style, and Code Quality
Professionals do not trust code that has not been tested. Test-driven development (TDD) flips the usual order: write a failing test first, then write the smallest code that passes it, then refactor. A test uses assert: assert add(2, 3) == 5 passes silently and raises AssertionError if wrong. For bigger projects, the pytest library (pip install pytest) discovers and runs test functions named test_* and reports exactly which failed. Good tests cover the happy path (normal input), edge cases (empty list, zero, negative numbers), and error cases (invalid input raises the right exception).
Style is part of quality. Python's style guide PEP 8 recommends: 4-space indentation, snake_case for functions and variables, CamelCase for classes, lines under 79β88 characters, and one blank line between functions. Docstrings (triple-quoted strings under def) explain what a function does, its parameters, and what it returns. Tools like black (auto-formatter) and flake8 (style checker) enforce style automatically β professionals let tools handle formatting so humans can focus on logic.
Real-world application: continuous-integration systems run a project's full test suite on every change; a single failing test blocks the release. Tests are a safety net that lets teams change code fearlessly.
Worked example — a pytest file:
# test_math_utils.py
def add(a, b):
return a + b
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2
def test_add_zero():
assert add(0, 0) == 0
- Run
pytest in the folder: it reports 3 passed. Change add to subtract, and pytest shows exactly which tests fail and why.
- Each test checks one behavior β when one fails, you know precisely what broke.
Worked example — docstring and style:
def grade_letter(score):
"""Return the letter grade (A-F) for a numeric score 0-100."""
if score >= 90:
return "A"
if score >= 80:
return "B"
return "F"
- The docstring tells a reader the contract: input range, output values. The name
grade_letter beats gl or func1.
- Style check: 4-space indent,
snake_case, short lines β all PEP 8.
Try it yourself: Write 3 tests for a function is_even(n) (even, odd, zero). What does TDD stand for, and which step comes first? Answers: assert is_even(4) is True; assert is_even(3) is False; assert is_even(0) is True; Test-Driven Development β the test comes first.
Key vocabulary: TDD — test-driven development (test first, then code); pytest — a popular test runner; happy path / edge case / error case — normal / boundary / invalid inputs; PEP 8 — Python's style guide; docstring — a function's built-in documentation.
Lesson 18: Turtle Graphics and Building a GUI
So far your programs have lived in the terminal. Turtle graphics β built into Python β lets you draw with code: a turtle moves around a canvas leaving a trail. import turtle; t = turtle.Turtle(); t.forward(100); t.left(90) draws two sides of a square. A for loop draws the whole square; change the turn angle and you get stars, spirals, and polygons. Turtle teaches state (the turtle's position and heading) and geometry in the most visible way possible: every bug shows up as a wrong picture.
For real applications, Python's built-in tkinter library builds windows with buttons, labels, and text boxes. The pattern: create a window, add widgets, and bind buttons to functions β when the user clicks, your function runs. This is event-driven programming: instead of running top-to-bottom, the program waits for events (clicks, keypresses) and responds. Every app on your phone works this way.
Real-world application: calculators, settings screens, and game menus are all event-driven GUIs β the same button-and-function pattern, with fancier widgets.
Worked example — drawing a square and a star with turtle:
import turtle
t = turtle.Turtle()
for _ in range(4):
t.forward(100)
t.left(90) # square: exterior angle 360/4 = 90
- Star:
for _ in range(5): t.forward(100); t.left(144) β the 144Β° turn (180 β 36) makes the five-pointed star.
- General rule: a regular n-gon turns
360 / n degrees at each corner.
Worked example — a tkinter click counter:
import tkinter as tk
count = 0
window = tk.Tk()
label = tk.Label(window, text="Count: 0")
label.pack()
def clicked():
global count
count += 1
label.config(text=f"Count: {count}")
tk.Button(window, text="Click me!", command=clicked).pack()
window.mainloop()
mainloop() starts the event loop: the window waits, and each click calls clicked().
global count is needed because the function reassigns a variable from outside it.
Try it yourself: Draw a hexagon with turtle (turn angle?). Add a "Reset" button to the counter that sets the count back to 0. Answers: Hexagon: 360 / 6 = 60 degrees; Reset button calls a function setting count = 0 and updating the label.
Key vocabulary: Turtle graphics — drawing by moving a virtual pen; tkinter — Python's built-in GUI library; widget — a GUI element (button, label, entry); event-driven programming — code that waits for and responds to events; mainloop — the event loop that keeps a GUI alive.
Lesson 19: Data Visualization and an Introduction to Data Science
Data science is the pipeline of collecting, cleaning, analyzing, and visualizing data to answer questions. The most powerful step is often visualization: a chart reveals patterns β trends, clusters, outliers β that a table of numbers hides. Python's matplotlib library (pip install matplotlib) makes line charts, bar charts, and scatter plots in a few lines. A line chart shows change over time; a bar chart compares categories; a scatter plot shows the relationship between two variables (for example, hours studied vs. test score).
Before charting, clean the data: handle missing values, fix inconsistent spellings, and convert text numbers to real numbers β real datasets are always messy. Then summarize: the mean (average), median (middle value), and mode (most common value) describe the center; the range and standard deviation describe the spread. Averages can mislead: one billionaire in a room of ten students makes the "average wealth" enormous while the median stays ordinary β always ask which summary fits.
Real-world application: election forecasts, sports analytics, public-health dashboards, and business reports all run this pipeline: clean β summarize β visualize β decide.
Worked example — a line chart with matplotlib:
import matplotlib.pyplot as plt
weeks = [1, 2, 3, 4]
scores = [62, 71, 78, 88]
plt.plot(weeks, scores, marker="o")
plt.xlabel("Week")
plt.ylabel("Quiz score")
plt.title("My scores are improving!")
plt.show()
- The upward slope is visible instantly: scores rose about 8β9 points per week.
- Labels and a title are not decoration β a chart without them cannot be read.
Worked example — mean vs. median:
- Incomes:
[30, 32, 35, 38, 40, 45, 1000] (in thousands). Mean = (30+32+35+38+40+45+1000) / 7 = 1220 / 7 β 174.
- Median (middle value) =
38 β a far better picture of a typical income here.
- Lesson: when outliers exist, the median resists them; the mean does not. Report both when they disagree.
Try it yourself: Given [5, 8, 8, 9, 12], compute the mean and median. When would a bar chart beat a line chart? Answers: Mean = 42 / 5 = 8.4; median = 8; bar charts compare separate categories, line charts show change over time.
Key vocabulary: Data science — collecting, cleaning, analyzing, and visualizing data; visualization — charts that reveal patterns; matplotlib — Python's charting library; mean / median / mode — average / middle / most common value; outlier — an extreme value that distorts averages.
Lesson 20: Final Project — Build a Personal Finance Tracker
Your final project combines everything in this course: a Personal Finance Tracker that records income and expenses, sorts them by category, saves to a file, and shows a summary chart. Plan it as pieces before coding: (1) an Expense class (date, category, description, amount); (2) functions to add, list, and filter expenses; (3) JSON save/load for persistence; (4) a summary that totals spending by category; (5) a bar chart of spending by category with matplotlib; (6) a simple menu loop or tkinter GUI. Planning on paper first β classes, functions, file format β is what separates working programs from tangled ones.
Build in stages and test each stage: first the class and add/list functions with hard-coded data; then file saving; then the menu; then the chart. Use try/except around number parsing so a typo cannot crash the app, write docstrings for every function, and add at least five tests (adding, totaling, filtering, saving, loading). If a stage breaks, isolate it with a small test β the debugging strategy from Lesson 7.
Real-world application: budgeting apps like Mint and YNAB are this project at scale β the same classes, files, and summaries, plus bank connections and a polished interface. Your tracker is a genuine portfolio piece: put it on GitHub with a README explaining what it does and how to run it.
Worked example — the data model and a summary function:
class Expense:
def __init__(self, date, category, description, amount):
self.date = date
self.category = category
self.description = description
self.amount = amount
def total_by_category(expenses):
totals = {}
for e in expenses:
totals[e.category] = totals.get(e.category, 0) + e.amount
return totals
- With expenses of Food 12, Transport 5, Food 8:
total_by_category returns {'Food': 20, 'Transport': 5}.
- Saving:
[e.__dict__ for e in expenses] converts objects to dictionaries for json.dump; loading rebuilds objects from dictionaries.
Worked example — a project checklist (definition of done):
- Add/list/filter expenses; totals by category; JSON save/load; bar chart; input validation with
try/except.
- Five passing tests; docstrings on every function; README with run instructions.
- Stretch: monthly reports, a budget-limit warning ("Food spending is over $100 this month!"), or a tkinter GUI.
Try it yourself: Build the tracker in stages this week. As a final check: what happens if the JSON file is missing on first run, and how should your program handle it? Answers: json.load would raise FileNotFoundError β catch it and start with an empty list (the Lesson 6 pattern).
Key vocabulary: Portfolio project — a finished program demonstrating your skills; README — the file explaining a project; definition of done — the checklist a project must satisfy; persistence — saving data between runs.