What’s one thing you learned? What’s still confusing?
File I/O: Read, Write & Persist Data
Open files with `with`, handle errors gracefully, and work with pathlib, json, and csv.
Mini-Project: Todo App
Build a CLI todo app with file persistence — your first program that remembers state between runs.
Modules, Packages & pip
Import modules, install packages, create virtual environments, and structure projects.
Interactive Labs for This Track
Loop Visualizer
You're a factory robot repeating the same task on an assembly line — watch how loops automate repetitive work
List Slicing
You have a playlist of 50 songs — grab just tracks 10 through 20 with a single slice expression
Sorting Algorithms
You're organizing a library of 10,000 books — which sorting method is fastest?
Ask questions, share insights
A calculator that accepts two numbers and an operator (+, -, *, /). It survives any bad input -- non-numeric values, unknown operators, division by zero -- and gives a clear, friendly message before asking again. The user has to type "quit" to leave.
First number: ten
That doesn't look like a number. Try again.
First number: 10
Operator (+, -, *, /): plus
Unknown operator 'plus'. Use one of: + - * /
Operator (+, -, *, /): +
Second number: 0
10.0 + 0.0 = 10.0
First number: 10
Operator (+, -, *, /): /
Second number: 0
You can't divide by zero. Try again.
First number: quit
Bye!
try/exceptThe cheerful, fragile version. Works -- as long as nobody types anything weird.
This code calls input(). Python here can't pause to ask you, so type the answers below before running — one value per line, in the order the program asks for them.
a = float(input("First number: "))
op = input("Operator: ")
b = float(input("Second number: "))
if op == "+":
print(a + b)
elif op == "-":
print(a - b)
elif op == "*":
print(a * b)
elif op == "/":
print(a / b)ten for the first number. ValueError. Try / with b = 0. ZeroDivisionError. Try ?? for the operator. Silent nothing. We'll fix all three.What does this raise? result = 10 / 0
x / 0 — it raises a specific, named exception so you can catch it. Note that ZeroDivisionError is a subclass of ArithmeticError, so except ArithmeticError would catch it too — but catching the most specific type is always better. Floats are special: float('inf') / 0 also raises ZeroDivisionError, but 0.0 ** -1 raises it too while float('inf') + 1 happily returns inf. The takeaway: division by zero is always a Python error you must explicitly handle.What exception does `float('ten')` raise?
b is 0 and the operator is /, Python raises ZeroDivisionError -- catch it and print a message.try:
print(a / b)
except ZeroDivisionError:
print("You can't divide by zero.")try runs the risky code. If an exception of the matching type is raised, control jumps to the except block. Code after the try/except keeps running like nothing happened.What gets printed? try: x = int("hello") except ZeroDivisionError: print("zero") print("after")
try raises ValueError (from int("hello")), but the except only catches ZeroDivisionError. Since the types don't match, the exception is NOT caught — it propagates up and crashes the program. The print("after") never runs. Lesson: except only catches the exact type (or its subclasses) you specify. If you want a broader net, use except Exception: — but never use bare except: which catches even Ctrl+C and hides real bugs.float("ten") raises ValueError. Wrap the input + conversion in try/except too.This code calls input(). Python here can't pause to ask you, so type the answers below before running — one value per line, in the order the program asks for them.
try:
a = float(input("First number: "))
except ValueError:
print("That doesn't look like a number.")Now bad input prints a message instead of crashing. But the user is dropped out of the program. To keep them in, loop until they give valid input.
Why catch `ValueError` specifically instead of using a bare `except:` ?
A helper that keeps asking for a number until it gets one. Returns the float when valid.
This code calls input(). Python here can't pause to ask you, so type the answers below before running — one value per line, in the order the program asks for them.
def read_number(prompt):
while True:
raw = input(prompt)
if raw == "quit":
return None
try:
return float(raw)
except ValueError:
print("That doesn't look like a number. Try again.")while True + try/except + return is the universal "ask until valid" pattern. Returning None on "quit" lets the caller stop the main loop cleanly.What happens here? while True: try: x = int(input()) # user types "abc" break except ValueError: print("try again")
int("abc") raises ValueError, the except catches it and prints "try again". Since break was never reached, the while True keeps looping — so we ask for input again. If the user finally types a valid number, int() succeeds, break fires, and the loop ends. This is the universal "keep asking until valid" pattern. Without break inside the try, even valid input would loop forever; without try/except, bad input crashes the program.Operators are easier than numbers -- just check membership in a small set.
This code calls input(). Python here can't pause to ask you, so type the answers below before running — one value per line, in the order the program asks for them.
VALID_OPS = {"+", "-", "*", "/"}
def read_operator():
while True:
op = input("Operator (+, -, *, /): ").strip()
if op in VALID_OPS:
return op
print(f"Unknown operator {op!r}. Use one of: + - * /"){...} set syntax. op in VALID_OPS is O(1) lookup. {op!r} calls repr() so the message shows the operator with quotes -- helpful when the user typed something weird with spaces.def main():
while True:
a = read_number("First number: ")
if a is None:
break
op = read_operator()
b = read_number("Second number: ")
if b is None:
break
try:
if op == "+":
result = a + b
elif op == "-":
result = a - b
elif op == "*":
result = a * b
else:
result = a / b
print(f"{a} {op} {b} = {result}")
except ZeroDivisionError:
print("You can't divide by zero. Try again.")
print("Bye!")That's a calculator the user genuinely can't break. Try every kind of garbage -- it'll just ask again.
Build a calculator that accepts two numbers and an operator (+, -, *, /). The program must NEVER crash, no matter what the user types: not on letters, not on division by zero, not on unknown operators. Bad inputs print a friendly message and re-prompt. Typing 'quit' as a number ends the program.
First number: ten
That doesn't look like a number. Try again.
First number: 10
Operator (+, -, *, /): plus
Unknown operator 'plus'. Use one of: + - * /
Operator (+, -, *, /): +
Second number: 0
10.0 + 0.0 = 10.0
First number: 10
Operator (+, -, *, /): /
Second number: 0
You can't divide by zero. Try again.
First number: quit
Bye!VALID_OPS = {"+", "-", "*", "/"}
def read_number(prompt):
# TODO: while loop -- ask until valid float OR 'quit'
# TODO: return None on quit, float on success
pass
def read_operator():
# TODO: while loop -- ask until input is in VALID_OPS
pass
def main():
while True:
a = read_number("First number: ")
if a is None:
break
op = read_operator()
b = read_number("Second number: ")
if b is None:
break
# TODO: compute result based on op
# TODO: catch ZeroDivisionError for division
# TODO: print the result
print("Bye!")
main()
Try one of these variations:
** (power), % (modulo), and // (integer division). Update both VALID_OPS and the dispatch chain.OPS = {"+": lambda a, b: a + b, ...}. The dispatch becomes a single line: result = OPS[op](a, b).with open(...), handle missing files, and save model artifacts -- the foundation of every ML pipeline that loads training data from disk.