Mathew K Analytics

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…

⬇ Download notebook

📓 Full notebook

Download .ipynb

Learn 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))
Basil: water me today!
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))
Cactus: I'm fine for 11 more days.
Fern: water me tomorrow.
Day 1 -> Mint: I'm fine for 2 more days.
Day 2 -> Mint: water me tomorrow.
Day 3 -> Mint: water me today!

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])
['passport', 'charger', 'socks']
3 items
First item: passport
Last item: socks
packing.append("toothbrush")
packing.insert(0, "tickets")
print(packing)
['tickets', 'passport', 'charger', 'socks', 'toothbrush']
packing.remove("socks")
last_one = packing.pop()
print("Popped off the end:", last_one)
print(packing)
Popped off the end: toothbrush
['tickets', 'passport', 'charger']
print("charger" in packing)
print("umbrella" in packing)
packing.sort()
print(packing)
print("Essentials:", packing[:2])
True
False
['charger', 'passport', 'tickets']
Essentials: ['charger', 'passport']
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)
Heaviest item: 2.5 kg
Bag total: 5.2 kg
Lightest first: [0.3, 0.4, 0.8, 1.2, 2.5]
Original order: [1.2, 0.4, 0.3, 2.5, 0.8]
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}")
1. [ ] passport
2. [ ] charger
3. [ ] toothbrush
4. [ ] 3 t-shirts
5. [ ] 4 pairs of socks
6. [ ] umbrella

Common mistake: reaching past the end of a list#

print(trip[6])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[10], line 1
----> 1 print(trip[6])

IndexError: list index out of range
print(f"{len(trip)} items, so the last position is {len(trip) - 1}")
print(trip[5], "|", trip[-1])
6 items, so the last position is 5
umbrella | umbrella

Your turn#

  1. Add a beach parameter that adds sunscreen and a towel when it's True.
  2. 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"])
{'rice': 2, 'eggs': 6, 'pasta': 1}
Eggs left: 6
pantry["eggs"] = 4
pantry["coffee"] = 1
pantry.update({"rice": 3, "oats": 2})
print(pantry)
{'rice': 3, 'eggs': 4, 'pasta': 1, 'coffee': 1, 'oats': 2}

Common mistake: asking for a key that isn't there#

print(pantry["milk"])
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[14], line 1
----> 1 print(pantry["milk"])

KeyError: 'milk'
print(pantry.get("milk"))
print(pantry.get("milk", 0))
print("milk" in pantry, "rice" in pantry)
None
0
False True
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()))
rice      3  ok
eggs      4  ok
pasta     1  running low
coffee    1  running low
oats      2  ok
Total items: 11
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))
Buy: ['pasta', 'coffee']
Buy: ['rice', 'pasta', 'coffee', 'oats']
finished = pantry.pop("pasta")
print(f"Cooked all {finished} packet of pasta.")
print("Still in stock:", list(pantry))
Cooked all 1 packet of pasta.
Still in stock: ['rice', 'eggs', 'coffee', 'oats']

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")
Day 1: 35 pages to go
Day 2: 20 pages to go
Day 3: 5 pages to go
Day 4: 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}")
Book title (blank to stop): The Hobbit
Book title (blank to stop): Matilda
Book title (blank to stop): Dune
Book title (blank to stop): 
You logged 3 books: ['The Hobbit', 'Matilda', 'Dune']
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!")
[a] add pages  [s] show  [q] quit: a
  Which book? Dune
  Pages read: 40
[a] add pages  [s] show  [q] quit: a
  Which book? Matilda
  Pages read: 25
[a] add pages  [s] show  [q] quit: a
  Which book? Dune
  Pages read: 30
[a] add pages  [s] show  [q] quit: x
  Unknown option, try again.
[a] add pages  [s] show  [q] quit: s
  Dune: 70 pages
  Matilda: 25 pages
[a] add pages  [s] show  [q] quit: q
Happy reading!

Your turn#

  1. Add an [r] remove option that deletes a book with reading_log.pop(book, None).
  2. Make the show option print the total pages across all books.

Project 5: The Ticket Booth (Handling Bad Input)#

Users mistype. Good programs expect it: try the risky line, except the error you expect.

Common mistake: trusting the user to type a number#

tickets = int("three")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[22], line 1
----> 1 tickets = int("three")

ValueError: invalid literal for int() with base 10: 'three'
try:
    tickets = int("three")
except ValueError:
    print("Please type the number with digits, like 3.")
print("The program is still running!")
Please type the number with digits, like 3.
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")
How many seats? two
  'two' isn't a whole number. Try again.
How many seats? 2.5
  '2.5' isn't a whole number. Try again.
How many seats? 2
Booking 2 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)
'12' became the number 12
  finished checking 12
'twelve' is not a number
  finished checking twelve
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))
25.0
  Sorry, we don't sell 'pirate' tickets.
0.0
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}")
Ticket type (adult/child/senior, or done): Adult
  How many adult tickets? 2
Ticket type (adult/child/senior, or done): toddler
  We only sell: adult, child, senior
Ticket type (adult/child/senior, or done): child
  How many child tickets? lots
  'lots' isn't a whole number. Try again.
  How many child tickets? 3
Ticket type (adult/child/senior, or done): done
Total to pay: £46.00

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"])
{'north': 'gallery', 'east': 'gift shop'}
gallery
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")
== Gift Shop ==
  Postcards everywhere. A heavy door leads east.
  Exits: west, east
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)
== Lobby ==
  Marble floors. A guard snores by the door.
  Exits: north, east
Which way? east
== Gift Shop ==
  Postcards everywhere. A heavy door leads east.
  Exits: west, east
Which way? east
  The vault door is locked. If only you had a key...
Which way? up
  You bump into a wall.
Which way? west
== Lobby ==
  Marble floors. A guard snores by the door.
  Exits: north, east
Which way? north
== Gallery ==
  Portraits stare. Something glints on a bench.
  Exits: south
  You pick up the brass key.
Which way? south
== Lobby ==
  Marble floors. A guard snores by the door.
  Exits: north, east
Which way? east
== Gift Shop ==
  Postcards everywhere. A heavy door leads east.
  Exits: west, east
Which way? east
== Vault ==
  The clockwork owl blinks and hoots. You found it!
Backpack: ['brass key']

Your turn#

  1. Add a "basement" room below the lobby with its own description.
  2. Make the player type look to 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.")
Journal created.
with open("journal.txt", "r") as journal:
    print(journal.read())
Mon | 7 | Finished my first Python project
Tue | 5 | Rainy, stayed in and 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.")
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")
Mon: *******    Finished my first Python project
Tue: *****      Rainy, stayed in and read
Wed: ********   Fixed a bug all by myself
Thu: ******     Long day, short walk
Average mood: 6.5 out of 10
with open("journal.txt") as journal:
    lines = journal.readlines()
print(len(lines), "lines")
print(repr(lines[0]))
4 lines
'Mon | 7 | Finished my first Python project\n'

Common mistake: a typo in the file name#

with open("jornal.txt") as journal:
    print(journal.read())
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[36], line 1
----> 1 with open("jornal.txt") as journal:
      2     print(journal.read())

File ~\AppData\Roaming\Python\Python312\site-packages\IPython\core\interactiveshell.py:324, in _modified_open(file, *args, **kwargs)
    317 if file in {0, 1, 2}:
    318     raise ValueError(
    319         f"IPython won't let you open fd={file} by default "
    320         "as it is likely to crash IPython. If you know what you are doing, "
    321         "you can use builtins' open."
    322     )
--> 324 return io_open(file, *args, **kwargs)

FileNotFoundError: [Errno 2] No such file or directory: 'jornal.txt'
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")
[]
4 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))
Bristol

3 contacts: ['Amara', 'Jonas', 'Mei']
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)
Saved 3 contacts to contacts.txt
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)
True
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"))
leeds -> ['Amara', 'Mei']
jo    -> ['Jonas']
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")
Mei: 0161 496 0456 (Leeds)
No contact called 'zara'. Try: Amara, Jonas, Mei

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")
Saved 160 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])
28 words
['the', 'lighthouse', 'keeper', 'counted', 'the', 'ships', 'the', 'ships']
counts = {}
for word in words:
    counts[word] = counts.get(word, 0) + 1
print(counts["the"], counts["ships"], counts["dawn"])
7 3 1
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}")
the       ####### 7
counted   ### 3
ships     ### 3
keeper    ## 2
stars     ## 2
longest = max(counts, key=len)
print(f"Longest word: {longest} ({len(longest)} letters)")
print("Different words:", len(counts))
Longest word: lighthouse (10 letters)
Different words: 14
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"))
[('counted', 3), ('ships', 3), ('keeper', 2)]

Your turn#

  1. Write a few sentences of your own to a file and find its top five words.
  2. 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"]))
Raffle ticket: 328
Tonight's dinner: ramen
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)
Captains: ['Ada', 'Fay']
Shuffled: ['Hana', 'Dara', 'Chen', 'Ada', 'Fay', 'Gus', 'Ben', 'Eli']
Original: ['Ada', 'Ben', 'Chen', 'Dara', 'Eli', 'Fay', 'Gus', 'Hana']
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))
Team 1 (3): Gus, Eli, Ben
Team 2 (3): Hana, Ada, Fay
Team 3 (2): Chen, Dara
Bringing snacks: Fay
tally = {}
for _ in range(800):
    name = random.choice(players)
    tally[name] = tally.get(name, 0) + 1
print(tally)
{'Ada': 101, 'Dara': 108, 'Ben': 107, 'Gus': 97, 'Chen': 92, 'Eli': 103, 'Fay': 86, 'Hana': 106}

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)
Priya has 10.0
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)
Beep! Kings Cross. Balance £2.20
Gate closed at Camden: only £2.20 left.
Beep! Camden. Balance £9.40
Priya's card: £9.40, 2 trips
Trips: ['Kings Cross', 'Camden']
tom = TravelCard("Tom")
tom.tap("Brixton")
print(tom)
print(priya)
Gate closed at Brixton: only £0.00 left.
Tom's card: £0.00, 0 trips
Priya's card: £9.40, 2 trips
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}")
  Priya's card: £9.40, 2 trips
  Tom's card: £0.00, 0 trips
  Ola's card: £20.00, 0 trips
Money on all cards: £29.40
try:
    tom.top_up(-20)
except ValueError as problem:
    print("Top-up refused:", problem)
Top-up refused: Top-up amount must be positive

Common mistake: forgetting a required argument#

Pro tip: self is how a method reaches its own object's data. Forget it and nothing works.

mystery = TravelCard()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[60], line 1
----> 1 mystery = TravelCard()

TypeError: TravelCard.__init__() missing 1 required positional argument: 'owner'
sam = TravelCard("Sam", 3.00)
print(sam)
Sam's card: £3.00, 0 trips

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()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[62], line 8
      5     def greet(self):
      6         print("Hello,", owner)
----> 8 BrokenCard("Kai").greet()

Cell In[62], line 6, in BrokenCard.greet(self)
      5 def greet(self):
----> 6     print("Hello,", owner)

NameError: name 'owner' is not defined
class FixedCard:
    def __init__(self, owner):
        self.owner = owner

    def greet(self):
        print("Hello,", self.owner)

FixedCard("Kai").greet()
Hello, Kai

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))
Beep! Bank. Balance £1.60
Beep! Angel. Balance £0.20
Gate closed at Oval: only £0.20 left.
Leo's card: £0.20, 2 trips (student, Riverside College)
True

Your turn#

  1. Give TravelCard a history() method that prints each trip on its own line.
  2. Create a SeniorCard class 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 list shows what's installed
  • pip install --upgrade requests gets the newest version
  • pip freeze > requirements.txt records your project's packages
  • pip install -r requirements.txt installs 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()
No saved habits yet, starting fresh.
  Added habit: Read 10 pages
  Added habit: Stretch
  'Stretch' is already on your list.
  Stretch done on Mon. Streak building!
  Read 10 pages        0 day(s)
  Stretch        #     1 day(s)
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)
[a]dd [d]one [r]eport [s]ave & quit: a
  New habit: Drink water
  Added habit: Drink water
[a]dd [d]one [r]eport [s]ave & quit: d
  Which habit? Read 10 pages
  Which day? Mon
  Read 10 pages done on Mon. Streak building!
[a]dd [d]one [r]eport [s]ave & quit: d
  Which habit? Read 10 pages
  Which day? Tue
  Read 10 pages done on Tue. Future you says thanks.
[a]dd [d]one [r]eport [s]ave & quit: d
  Which habit? Juggling
  Which day? Tue
  No habit called 'Juggling'. Add it first with a.
[a]dd [d]one [r]eport [s]ave & quit: d
  Which habit? Drink water
  Which day? Tue
  Drink water done on Tue. Small steps, big results.
[a]dd [d]one [r]eport [s]ave & quit: x
  Please choose a, d, r or s.
[a]dd [d]one [r]eport [s]ave & quit: r
  Read 10 pages  ##    2 day(s)
  Stretch        #     1 day(s)
  Drink water    #     1 day(s)
[a]dd [d]one [r]eport [s]ave & quit: s
  Saved 3 habits to habits.txt
next_week = HabitTracker()
next_week.report()
print("Habits remembered:", list(next_week.habits))
  Read 10 pages  ##    2 day(s)
  Stretch        #     1 day(s)
  Drink water    #     1 day(s)
Habits remembered: ['Read 10 pages', 'Stretch', 'Drink water']

Recap#

  • Lists: append, insert, remove, pop, sort, slicing, in, enumerate
  • Dictionaries: d[key], get with a default, update, .items(), dictionaries inside dictionaries
  • while loops and break for menus and repeated input
  • try / except ValueError / KeyError / FileNotFoundError for friendly errors
  • Files: with open(...) and the modes w, a, r; read, readlines, split
  • random: seed, randint, choice, shuffle, sample
  • Classes: __init__, attributes, methods, __str__, raise, inheritance with super()

Pro tip: when a program gets big, ask: what are the things (classes) and what do they do (methods)?

Try it yourself#

  1. Habit streaks: add a best_habit() method that returns the habit with the most days.
  2. 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 to scores.txt.
  3. Recipe scaler: write a Recipe class with a dict of ingredients and a scale(people) method, then a VeganRecipe subclass 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.