Lesson 3 · Python Beginner Course
Learn Python by Building Projects: 12 Programs in One Hour (Beginner Python Course, Part 3)
In this lesson you'll be able to: Store and reshape collections of data with lists and dictionaries Build menu-driven programs with while loops and input…
- CoursePython Beginner Course
- Lesson3 of 10
- Video60 min
- FormatJupyter notebook · 70 code cells
What you'll learn
- Warm-up: The Plant Watering Reminder
- Project 2: The Packing List Builder (Lists)
- Project 3: The Pantry Stock Keeper (Dictionaries)
- Project 4: The Reading Log (While Loops and Menus)
- Project 5: The Ticket Booth (Handling Bad Input)
- Project 6: The Midnight Museum (A Tiny Text Adventure)
- Project 7: The Mood Journal (Reading and Writing Files)
- Project 8: The Contact Book (Saving a Dictionary to a File)
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbLearn Python by Building Projects: 12 Programs, One Hour#
In this lesson you'll be able to:
- Store and reshape collections of data with lists and dictionaries
- Build menu-driven programs with while loops and input
- Catch bad input with try and except instead of crashing
- Save your data to text files and load it back later
- Add surprise and fairness with the random module
- Model everyday things with classes, objects and inheritance Projects: a packing list, pantry keeper, reading log, ticket booth, museum adventure, mood journal, contact book, word counter, team picker, travel card and habit tracker.
Warm-up: The Plant Watering Reminder#
Project 1: a tiny script that tells you which plants are thirsty.
Pro tip: a function should do one job and hand back a result with return.
def watering_message(plant, days_since, every_n_days):
days_left = every_n_days - days_since
if days_left <= 0:
return f"{plant}: water me today!"
elif days_left == 1:
return f"{plant}: water me tomorrow."
else:
return f"{plant}: I'm fine for {days_left} more days."
print(watering_message("Basil", 2, 2))
print(watering_message("Cactus", 3, 14))
print(watering_message("Fern", 4, 5))
for day in range(1, 4):
print(f"Day {day} ->", watering_message("Mint", day, 3))
Project 2: The Packing List Builder (Lists)#
A list is an ordered row of values, like a row of numbered lockers starting at locker 0.
Pro tip: name lists with plural nouns (items, guests, scores) so loops read like English.
packing = ["passport", "charger", "socks"]
print(packing)
print(len(packing), "items")
print("First item:", packing[0])
print("Last item:", packing[-1])
packing.append("toothbrush")
packing.insert(0, "tickets")
print(packing)
packing.remove("socks")
last_one = packing.pop()
print("Popped off the end:", last_one)
print(packing)
print("charger" in packing)
print("umbrella" in packing)
packing.sort()
print(packing)
print("Essentials:", packing[:2])
weights_kg = [1.2, 0.4, 0.3, 2.5, 0.8]
print("Heaviest item:", max(weights_kg), "kg")
print("Bag total:", round(sum(weights_kg), 1), "kg")
print("Lightest first:", sorted(weights_kg))
print("Original order:", weights_kg)
def build_packing_list(nights, rainy):
items = ["passport", "charger", "toothbrush"]
items.append(f"{nights} t-shirts")
items.append(f"{nights + 1} pairs of socks")
if rainy:
items.append("umbrella")
return items
trip = build_packing_list(3, rainy=True)
for number, item in enumerate(trip, start=1):
print(f"{number}. [ ] {item}")
Common mistake: reaching past the end of a list#
print(trip[6])
print(f"{len(trip)} items, so the last position is {len(trip) - 1}")
print(trip[5], "|", trip[-1])
Your turn#
- Add a
beachparameter that adds sunscreen and a towel when it's True. - Print how many items are on the list after building it.
Project 3: The Pantry Stock Keeper (Dictionaries)#
A dictionary maps keys to values, like a coat check: hand over the ticket, get back the coat.
Pro tip: if you find yourself searching a list for a name, you probably wanted a dictionary.
pantry = {"rice": 2, "eggs": 6, "pasta": 1}
print(pantry)
print("Eggs left:", pantry["eggs"])
pantry["eggs"] = 4
pantry["coffee"] = 1
pantry.update({"rice": 3, "oats": 2})
print(pantry)
Common mistake: asking for a key that isn't there#
print(pantry["milk"])
print(pantry.get("milk"))
print(pantry.get("milk", 0))
print("milk" in pantry, "rice" in pantry)
for food, amount in pantry.items():
status = "running low" if amount <= 1 else "ok"
print(f"{food:<8}{amount:>3} {status}")
print("Total items:", sum(pantry.values()))
def shopping_list(stock, minimum=2):
to_buy = []
for food, amount in stock.items():
if amount < minimum:
to_buy.append(food)
return to_buy
print("Buy:", shopping_list(pantry))
print("Buy:", shopping_list(pantry, minimum=4))
finished = pantry.pop("pasta")
print(f"Cooked all {finished} packet of pasta.")
print("Still in stock:", list(pantry))
Quick check#
What does pantry.get("flour", 0) give you, and why doesn't it crash?
Project 4: The Reading Log (While Loops and Menus)#
Use for when you know how many times. Use while when you repeat until something happens.
Pro tip: every while loop needs a way out. Before you run it, point to the line that ends it.
pages_left = 50
day = 0
while pages_left > 0:
day += 1
pages_left -= 15
print(f"Day {day}: {max(pages_left, 0)} pages to go")
books = []
while True:
title = input("Book title (blank to stop): ")
if title == "":
break
books.append(title)
print(f"You logged {len(books)} books: {books}")
reading_log = {}
choice = ""
while choice != "q":
choice = input("[a] add pages [s] show [q] quit: ").strip().lower()
if choice == "a":
book = input(" Which book? ")
pages = int(input(" Pages read: "))
reading_log[book] = reading_log.get(book, 0) + pages
elif choice == "s":
for book, pages in reading_log.items():
print(f" {book}: {pages} pages")
elif choice != "q":
print(" Unknown option, try again.")
print("Happy reading!")
Your turn#
- Add an
[r] removeoption that deletes a book withreading_log.pop(book, None). - Make the show option print the total pages across all books.
tickets = int("three")
try:
tickets = int("three")
except ValueError:
print("Please type the number with digits, like 3.")
print("The program is still running!")
def ask_whole_number(prompt):
while True:
answer = input(prompt)
try:
return int(answer)
except ValueError:
print(f" '{answer}' isn't a whole number. Try again.")
seats = ask_whole_number("How many seats? ")
print("Booking", seats, "seats")
for text in ["12", "twelve"]:
try:
number = int(text)
except ValueError:
print(f"'{text}' is not a number")
else:
print(f"'{text}' became the number {number}")
finally:
print(" finished checking", text)
prices = {"adult": 12.50, "child": 7.00, "senior": 9.00}
def ticket_cost(kind, count):
try:
return prices[kind] * count
except KeyError:
print(f" Sorry, we don't sell '{kind}' tickets.")
return 0.0
print(ticket_cost("adult", 2))
print(ticket_cost("pirate", 1))
total = 0.0
while True:
kind = input("Ticket type (adult/child/senior, or done): ").strip().lower()
if kind == "done":
break
if kind not in prices:
print(" We only sell:", ", ".join(prices))
continue
count = ask_whole_number(f" How many {kind} tickets? ")
total += ticket_cost(kind, count)
print(f"Total to pay: £{total:.2f}")
Pro tip: catch the specific error you expect (ValueError, KeyError), never a bare
except:.Quick check#
What error does
int("4.5")raise, and what would you write to catch it?
Project 6: The Midnight Museum (A Tiny Text Adventure)#
A dictionary can hold other dictionaries. Each room knows its description and its exits.
Pro tip: describe your world as data first. Then the game loop stays short and simple.
rooms = {
"lobby": {"text": "Marble floors. A guard snores by the door.",
"exits": {"north": "gallery", "east": "gift shop"}},
"gallery": {"text": "Portraits stare. Something glints on a bench.",
"exits": {"south": "lobby"}, "item": "brass key"},
"gift shop": {"text": "Postcards everywhere. A heavy door leads east.",
"exits": {"west": "lobby", "east": "vault"}},
"vault": {"text": "The clockwork owl blinks and hoots. You found it!",
"exits": {}},
}
print(rooms["lobby"]["exits"])
print(rooms["lobby"]["exits"]["north"])
def describe(room_name):
room = rooms[room_name]
print(f"== {room_name.title()} ==")
print(" " + room["text"])
if room["exits"]:
print(" Exits:", ", ".join(room["exits"]))
describe("gift shop")
location = "lobby"
backpack = []
describe(location)
while location != "vault":
room = rooms[location]
move = input("Which way? ").strip().lower()
if move not in room["exits"]:
print(" You bump into a wall.")
elif room["exits"][move] == "vault" and "brass key" not in backpack:
print(" The vault door is locked. If only you had a key...")
else:
location = room["exits"][move]
describe(location)
item = rooms[location].get("item")
if item and item not in backpack:
backpack.append(item)
print(f" You pick up the {item}.")
print("Backpack:", backpack)
Your turn#
- Add a
"basement"room below the lobby with its own description. - Make the player type
lookto describe the current room again.
Project 7: The Mood Journal (Reading and Writing Files)#
Variables vanish when a program ends. Files remember.
Modes: "w" write (wipes the file first), "a" append (adds to the end), "r" read.
with open("journal.txt", "w") as journal:
journal.write("Mon | 7 | Finished my first Python project\n")
journal.write("Tue | 5 | Rainy, stayed in and read\n")
print("Journal created.")
with open("journal.txt", "r") as journal:
print(journal.read())
def add_entry(day, mood, note):
with open("journal.txt", "a") as journal:
journal.write(f"{day} | {mood} | {note}\n")
add_entry("Wed", 8, "Fixed a bug all by myself")
add_entry("Thu", 6, "Long day, short walk")
print("Two entries added.")
moods = []
with open("journal.txt") as journal:
for line in journal:
day, mood, note = line.strip().split(" | ")
moods.append(int(mood))
print(f"{day}: {'*' * int(mood):<10} {note}")
print(f"Average mood: {sum(moods) / len(moods):.1f} out of 10")
with open("journal.txt") as journal:
lines = journal.readlines()
print(len(lines), "lines")
print(repr(lines[0]))
Common mistake: a typo in the file name#
with open("jornal.txt") as journal:
print(journal.read())
def load_lines(filename):
try:
with open(filename) as f:
return f.read().splitlines()
except FileNotFoundError:
return []
print(load_lines("jornal.txt"))
print(len(load_lines("journal.txt")), "entries")
Pro tip:
"w"empties the file the moment it opens. When in doubt, append with"a".
Project 8: The Contact Book (Saving a Dictionary to a File)#
One line per contact, fields separated by commas: Amara,0161 496 0000,Leeds
contacts = {
"Amara": {"phone": "0161 496 0000", "city": "Leeds"},
"Jonas": {"phone": "0161 496 0123", "city": "Bristol"},
}
contacts["Mei"] = {"phone": "0161 496 0456", "city": "Leeds"}
print(contacts["Jonas"]["city"])
print(len(contacts), "contacts:", list(contacts))
def save_contacts(book, filename="contacts.txt"):
with open(filename, "w") as f:
for name, info in book.items():
f.write(f"{name},{info['phone']},{info['city']}\n")
print(f"Saved {len(book)} contacts to {filename}")
save_contacts(contacts)
def load_contacts(filename="contacts.txt"):
book = {}
try:
with open(filename) as f:
for line in f:
name, phone, city = line.strip().split(",")
book[name] = {"phone": phone, "city": city}
except FileNotFoundError:
print("No contacts file yet.")
return book
loaded = load_contacts()
print(loaded == contacts)
def search(book, text):
text = text.lower()
matches = []
for name, info in book.items():
if text in name.lower() or text in info["city"].lower():
matches.append(name)
return matches
print("leeds ->", search(loaded, "leeds"))
print("jo ->", search(loaded, "jo"))
print("paris ->", search(loaded, "paris"))
def show_contact(book, name):
try:
info = book[name.title()]
except KeyError:
print(f"No contact called '{name}'. Try: {', '.join(book)}")
return
print(f"{name.title()}: {info['phone']} ({info['city']})")
show_contact(loaded, "mei")
show_contact(loaded, "zara")
Quick check#
Why does save_contacts use mode "w" while the mood journal's add_entry used "a"?
Project 9: The Word Frequency Counter#
Files plus dictionaries: read some text, count every word, rank the winners.
story = """The lighthouse keeper counted the ships.
The ships counted the stars, and the stars counted nothing at all.
At dawn the keeper slept, and the ships sailed home."""
with open("lighthouse.txt", "w") as f:
f.write(story)
print("Saved", len(story), "characters")
with open("lighthouse.txt") as f:
text = f.read().lower()
for mark in ".,!?":
text = text.replace(mark, "")
words = text.split()
print(len(words), "words")
print(words[:8])
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts["the"], counts["ships"], counts["dawn"])
def by_count(pair):
return pair[1]
ranked = sorted(counts.items(), key=by_count, reverse=True)
for word, n in ranked[:5]:
print(f"{word:<10}{'#' * n} {n}")
longest = max(counts, key=len)
print(f"Longest word: {longest} ({len(longest)} letters)")
print("Different words:", len(counts))
def top_words(filename, how_many=3):
boring = ["the", "and", "at", "all", "a"]
with open(filename) as f:
text = f.read().lower()
for mark in ".,!?":
text = text.replace(mark, "")
counts = {}
for word in text.split():
if word not in boring:
counts[word] = counts.get(word, 0) + 1
return sorted(counts.items(), key=by_count, reverse=True)[:how_many]
print(top_words("lighthouse.txt"))
Your turn#
- Write a few sentences of your own to a file and find its top five words.
- Add
"keeper"to the boring list and see how the ranking changes.
Project 10: The Random Team Picker#
The random module ships with Python. Import it once, then pick, shuffle and sample.
Pro tip:
random.seed(...)makes the "random" results repeatable, which is perfect for testing.
import random
random.seed(42)
print("Raffle ticket:", random.randint(1, 500))
print("Tonight's dinner:", random.choice(["ramen", "tacos", "curry", "pizza"]))
players = ["Ada", "Ben", "Chen", "Dara", "Eli", "Fay", "Gus", "Hana"]
print(f"Captains: {random.sample(players, 2)}")
line_up = players.copy()
random.shuffle(line_up)
print("Shuffled:", line_up)
print("Original:", players)
def pick_teams(names, team_count):
pool = names.copy()
random.shuffle(pool)
teams = []
for _ in range(team_count):
teams.append([])
for position, name in enumerate(pool):
teams[position % team_count].append(name)
return teams
random.seed(7)
teams = pick_teams(players, 3)
for number, team in enumerate(teams, start=1):
print(f"Team {number} ({len(team)}): {', '.join(team)}")
print("Bringing snacks:", random.choice(players))
tally = {}
for _ in range(800):
name = random.choice(players)
tally[name] = tally.get(name, 0) + 1
print(tally)
Quick check#
Why did pick_teams shuffle names.copy() instead of names?
Project 11: The Travel Card (Classes and Objects)#
A class is a design for a card; each card printed from it is an object with its own balance. Attributes are the data an object carries. Methods are the actions it can do.
class TravelCard:
def __init__(self, owner, balance=0.0):
self.owner = owner
self.balance = balance
card = TravelCard("Priya", 10.0)
print(card.owner, "has", card.balance)
class TravelCard:
fare = 2.80
def __init__(self, owner, balance=0.0):
self.owner = owner
self.balance = balance
self.trips = []
def top_up(self, amount):
if amount <= 0:
raise ValueError("Top-up amount must be positive")
self.balance += amount
def tap(self, station):
if self.balance < self.fare:
print(f"Gate closed at {station}: only £{self.balance:.2f} left.")
return False
self.balance -= self.fare
self.trips.append(station)
print(f"Beep! {station}. Balance £{self.balance:.2f}")
return True
def __str__(self):
return f"{self.owner}'s card: £{self.balance:.2f}, {len(self.trips)} trips"
priya = TravelCard("Priya", 5.00)
priya.tap("Kings Cross")
priya.tap("Camden")
priya.top_up(10)
priya.tap("Camden")
print(priya)
print("Trips:", priya.trips)
tom = TravelCard("Tom")
tom.tap("Brixton")
print(tom)
print(priya)
cards = [priya, tom, TravelCard("Ola", 20.0)]
total = 0.0
for card in cards:
print(" ", card)
total += card.balance
print(f"Money on all cards: £{total:.2f}")
try:
tom.top_up(-20)
except ValueError as problem:
print("Top-up refused:", problem)
Common mistake: forgetting a required argument#
Pro tip:
selfis how a method reaches its own object's data. Forget it and nothing works.
mystery = TravelCard()
sam = TravelCard("Sam", 3.00)
print(sam)
Common mistake: forgetting self inside a method#
class BrokenCard:
def __init__(self, owner):
self.owner = owner
def greet(self):
print("Hello,", owner)
BrokenCard("Kai").greet()
class FixedCard:
def __init__(self, owner):
self.owner = owner
def greet(self):
print("Hello,", self.owner)
FixedCard("Kai").greet()
Inheritance: Upgrading to a Student Card#
A new class can inherit everything from an existing one, then change only what's different.
class StudentCard(TravelCard):
fare = 1.40
def __init__(self, owner, school, balance=0.0):
super().__init__(owner, balance)
self.school = school
def __str__(self):
return f"{super().__str__()} (student, {self.school})"
leo = StudentCard("Leo", "Riverside College", 3.00)
leo.tap("Bank")
leo.tap("Angel")
leo.tap("Oval")
print(leo)
print(isinstance(leo, TravelCard))
Your turn#
- Give
TravelCardahistory()method that prints each trip on its own line. - Create a
SeniorCardclass whose fare is 0.00, and tap it a few times.
Installing Packages with pip#
Python's standard library is big, but thousands of free packages live on PyPI, the Python Package Index.
pip downloads and installs them. You run it in a terminal, not inside your Python code:
pip install requests
On Mac or Linux you may need: python3 -m pip install requests
Then use it in your program like any module: import requests
Handy pip commands:
pip listshows what's installedpip install --upgrade requestsgets the newest versionpip freeze > requirements.txtrecords your project's packagespip install -r requirements.txtinstalls them all on another computer
Pro tip: give each project its own virtual environment (
python -m venv .venv) so their packages never clash.
Capstone: The Habit Tracker#
Everything at once: a class, a dictionary of lists, a while-loop menu, try and except, the random module, and a text file so your progress survives a restart. File format, one habit per line: Stretch|Mon,Tue
def save_habits(habits, filename):
with open(filename, "w") as f:
for name, days in habits.items():
f.write(f"{name}|{','.join(days)}\n")
def load_habits(filename):
habits = {}
try:
with open(filename) as f:
for line in f:
name, days = line.strip().split("|")
habits[name] = days.split(",") if days else []
except FileNotFoundError:
print("No saved habits yet, starting fresh.")
return habits
CHEERS = ["Nice work!", "Streak building!", "Future you says thanks.",
"Small steps, big results."]
class HabitTracker:
def __init__(self, filename="habits.txt"):
self.filename = filename
self.habits = load_habits(filename)
def add(self, name):
if name in self.habits:
print(f" '{name}' is already on your list.")
else:
self.habits[name] = []
print(f" Added habit: {name}")
def check_off(self, name, day):
self.habits[name].append(day)
print(f" {name} done on {day}. {random.choice(CHEERS)}")
def report(self):
for name, days in self.habits.items():
print(f" {name:<15}{'#' * len(days):<6}{len(days)} day(s)")
def save(self):
save_habits(self.habits, self.filename)
print(f" Saved {len(self.habits)} habits to {self.filename}")
random.seed(3)
tracker = HabitTracker()
tracker.add("Read 10 pages")
tracker.add("Stretch")
tracker.add("Stretch")
tracker.check_off("Stretch", "Mon")
tracker.report()
def run_menu(tracker):
while True:
choice = input("[a]dd [d]one [r]eport [s]ave & quit: ").strip().lower()
if choice == "a":
tracker.add(input(" New habit: "))
elif choice == "d":
name = input(" Which habit? ")
day = input(" Which day? ")
try:
tracker.check_off(name, day)
except KeyError:
print(f" No habit called '{name}'. Add it first with a.")
elif choice == "r":
tracker.report()
elif choice == "s":
tracker.save()
break
else:
print(" Please choose a, d, r or s.")
random.seed(8)
run_menu(tracker)
next_week = HabitTracker()
next_week.report()
print("Habits remembered:", list(next_week.habits))
Recap#
- Lists: append, insert, remove, pop, sort, slicing,
in,enumerate - Dictionaries:
d[key],getwith a default,update,.items(), dictionaries inside dictionaries whileloops andbreakfor menus and repeated inputtry/except ValueError/KeyError/FileNotFoundErrorfor friendly errors- Files:
with open(...)and the modesw,a,r;read,readlines,split random:seed,randint,choice,shuffle,sample- Classes:
__init__, attributes, methods,__str__,raise, inheritance withsuper()
Pro tip: when a program gets big, ask: what are the things (classes) and what do they do (methods)?
Try it yourself#
- Habit streaks: add a
best_habit()method that returns the habit with the most days. - Flashcard trainer: store words and meanings in a dictionary, quiz yourself in random order
with
random.shuffle, keep score, and append each session's score toscores.txt. - Recipe scaler: write a
Recipeclass with a dict of ingredients and ascale(people)method, then aVeganRecipesubclass that refuses to add eggs or milk.
What's next#
Lesson 4: Python Masterclass: Data Structures, OOP and Automation. Tuples, sets, comprehensions, deeper object-oriented design, and scripts that do chores for you.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



