What’s one thing you learned? What’s still confusing?
Modules, Packages & pip
Import modules, install packages, create virtual environments, and structure projects.
Environment Variables & Secrets: Never Hardcode API Keys
Keep API keys out of code using os.environ, .env files, and python-dotenv.
Calling APIs in Python: requests, JSON & Authentication
Master the requests library — GET/POST, JSON parsing, API auth, error handling.
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
todos.txt automatically so when you reopen the app tomorrow, your todos are still there.Todo App
========
1. add
2. list
3. done
4. quit
Choose: 1
Task: buy milk
Added.
Choose: 1
Task: write blog post
Added.
Choose: 2
[ ] 1. buy milk
[ ] 2. write blog post
Choose: 3
Which task number? 1
Marked done.
Choose: 2
[x] 1. buy milk
[ ] 2. write blog post
Choose: 4
Saved. Bye!
open() and the with statementtext and done.todos = []
todos.append({"text": "buy milk", "done": False})
todos.append({"text": "write blog post", "done": False})
for i, t in enumerate(todos, start=1):
mark = "x" if t["done"] else " "
print(f"[{mark}] {i}. {t['text']}")enumerate(seq, start=1) numbers items starting at 1 instead of 0 -- the right choice for human-facing lists.What does this print? items = ["a", "b", "c"] for i, x in enumerate(items): print(i, x)
enumerate() defaults to starting at 0 — same as Python's zero-indexed lists. The first value is the INDEX (0, 1, 2), the second is the item. Common bug: writing for x, i in enumerate(items) swaps the assignment and gives confusing output. To start counting at 1 for user-facing display, pass start=1: enumerate(items, start=1) yields (1, 'a'), (2, 'b'), (3, 'c').Why model each todo as a dict instead of just a string?
while True loop, a match on the user's choice, and a break for quit.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.
todos = []
while True:
print("\n1. add 2. list 3. done 4. quit")
choice = input("Choose: ").strip()
if choice == "1":
text = input("Task: ").strip()
todos.append({"text": text, "done": False})
print("Added.")
elif choice == "2":
for i, t in enumerate(todos, start=1):
mark = "x" if t["done"] else " "
print(f"[{mark}] {i}. {t['text']}")
elif choice == "3":
n = int(input("Which task number? "))
todos[n - 1]["done"] = True
print("Marked done.")
elif choice == "4":
print("Bye!")
breakint(input(...)) - 1 converts the human-friendly number (1-based) back to the Python index (0-based). Run this -- you have a working in-memory todo app already.What happens? todos = [{"text": "buy milk", "done": False}] todos[0]["done"] = True print(todos[0]["done"])
dict[key] = new_value. The dict inside the list is the same dict before and after; we just modified its "done" value. Compare with tuples, which are immutable: (1, 2)[0] = 99 raises TypeError. This is also why mutable objects (lists, dicts, sets) can be shared via aliasing — modifying through one reference is visible through all references that point to the same object.open(path, "w") to write and open(path, "r") to read. The with statement ensures the file is closed even if an error happens mid-write.def save(todos, path="todos.txt"):
with open(path, "w") as f:
for t in todos:
mark = "x" if t["done"] else "o"
f.write(f"{mark}|{t['text']}\n")x|task text (done) or o|task text (not done). The | is a delimiter -- a tiny custom format that's easy to parse back.Why use `with open(...) as f:` instead of plain `f = open(...)`?
On startup, read the file and rebuild the list. Handle the "file doesn't exist yet" case so first-run users don't crash.
import os
def load(path="todos.txt"):
if not os.path.exists(path):
return []
todos = []
with open(path, "r") as f:
for line in f:
line = line.rstrip("\n")
if not line:
continue
mark, text = line.split("|", 1)
todos.append({"text": text, "done": mark == "x"})
return todosline.split("|", 1) splits at most ONCE -- so even if the task text contains a |, only the first one separates the mark from the body. That's the kind of edge case worth thinking about up front.What happens if todos.txt does not exist? with open("todos.txt", "r") as f: content = f.read()
"r") does NOT create files — if the path doesn't exist, Python crashes. Write mode ("w") and append mode ("a") DO create the file if missing, but they treat existing files very differently ("w" overwrites, "a" appends). Three ways to handle missing files: check first with os.path.exists(path), catch the exception with try/except FileNotFoundError, or use pathlib.Path(path).read_text() then catch. The catch pattern is most Pythonic ("ask forgiveness, not permission").Load on startup. Save on every change (or only on quit -- your choice).
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.
import os
PATH = "todos.txt"
def load():
if not os.path.exists(PATH):
return []
todos = []
with open(PATH, "r") as f:
for line in f:
line = line.rstrip("\n")
if line:
mark, text = line.split("|", 1)
todos.append({"text": text, "done": mark == "x"})
return todos
def save(todos):
with open(PATH, "w") as f:
for t in todos:
mark = "x" if t["done"] else "o"
f.write(f"{mark}|{t['text']}\n")
def main():
todos = load()
print("Todo App")
while True:
print("\n1. add 2. list 3. done 4. quit")
choice = input("Choose: ").strip()
if choice == "1":
text = input("Task: ").strip()
todos.append({"text": text, "done": False})
save(todos)
elif choice == "2":
for i, t in enumerate(todos, start=1):
mark = "x" if t["done"] else " "
print(f"[{mark}] {i}. {t['text']}")
elif choice == "3":
n = int(input("Which task number? "))
todos[n - 1]["done"] = True
save(todos)
elif choice == "4":
save(todos)
print("Saved. Bye!")
break
main()That's a complete, persistent todo app -- in 35 lines of Python.
Build a CLI todo app with four commands: add, list, done, quit. State is a list of dicts with `text` and `done` keys. Persist to `todos.txt` so todos survive between runs. Use `with open(...)` for both reading and writing, and handle the case where the file doesn't exist yet.
Todo App
1. add 2. list 3. done 4. quit
Choose: 1
Task: buy milk
1. add 2. list 3. done 4. quit
Choose: 2
[ ] 1. buy milk
1. add 2. list 3. done 4. quit
Choose: 3
Which task number? 1
1. add 2. list 3. done 4. quit
Choose: 2
[x] 1. buy milk
1. add 2. list 3. done 4. quit
Choose: 4
Saved. Bye!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.
import os
PATH = "todos.txt"
def load():
# TODO: return [] if PATH doesn't exist
# TODO: open PATH, read each line, split on '|', append dicts
pass
def save(todos):
# TODO: write each todo as 'x|text' or 'o|text' lines
pass
def main():
todos = load()
print("Todo App")
while True:
print("\n1. add 2. list 3. done 4. quit")
choice = input("Choose: ").strip()
# TODO: handle each choice (1=add, 2=list, 3=done, 4=quit)
# TODO: call save() after every state change
# TODO: break on quit
pass
main()
Try one of these variations:
pop().work, home, errands). Filter the list view by category.|-delimited format for json.dump(todos, f) and json.load(f). Cleaner, no parser needed, and you can add new fields without breaking the file format.load/save can live in one file, the menu logic in another, and you'll start to feel what Python "projects" actually look like.