← Back to course
Code Crew G6+ / Week 12 / Tuesday
2/6
Week 12 Β· Ship and demo

Tuesday

Edge cases
// Test, polish, snapshot, graduate
⏱ about 30 min

Tuesday: Edge cases

Raven tries to break Mission Log on purpose, and she is very good at it. First she presses Enter at the search prompt without typing anything. Every entry comes back as a match.

Then she opens mission_log.json in IDLE, deletes the last few lines, and saves it. Mission Log crashes with a very long traceback before the menu even appears.

"What do you notice?" she asks Rocket, gently. "Neither one is a typo. They are things real users will do sooner or later."

Nova shows the two cases as small cracks in a wall. "Interesting," she says. "An edge case is any input at the very edge of what you planned for. The fix is to decide what should happen."

Rocket writes both cases on the plan and fixes them one at a time.

Edge case 1: an empty search

The empty string "" is inside every string, so "" in "anything" is True. An empty search matches every entry:

Mission Log ready. Entries loaded: 3

1 Add   2 List   3 Search   4 Stats   5 Quit
Choose 1 to 5: 3
Search for: 
1. 2026-04-10 [build] Built the launch clock
2. 2026-04-10 [test] Tested the clock with 65 seconds
3. 2026-04-11 [idea] Add a sound when the clock ends
Matches found: 3

1 Add   2 List   3 Search   4 Stats   5 Quit
Choose 1 to 5: 5
Log saved. See you next mission!

The fix: run_search checks for an empty word and asks for one. Replace run_search with this version:

def run_search(entries):
    """Ask for a word and print the matching entries."""
    word = input("Search for: ").strip()
    if word == "":
        print("Type a word to search for.")
        return
    found = search_entries(entries, word)
    for number, entry in enumerate(found, start=1):
        print(format_entry(number, entry))
    print(f"Matches found: {len(found)}")
Predict: what does "" in "Mission Control" give?
WHY THIS EXERCISEPython treats the empty string as part of every string, so the answer is True.

Edge case 2: a damaged file

When the JSON file is damaged, json.load raises a JSONDecodeError. Starting with an empty list would be worse: the next save would wipe out the old entries for good.

The safer choice is to stop and tell the user. The new load_entries gives back None, and main checks for it first.

Replace load_entries with this version:

def load_entries():
    """Read saved entries, or start an empty list on the first run.

    Gives back None if the file is damaged, so nothing is overwritten.
    """
    try:
        with open(LOG_FILE) as f:
            return json.load(f)
    except FileNotFoundError:
        return []
    except json.JSONDecodeError:
        return None

Then add these three lines in main, right after entries = load_entries():

    if entries is None:
        print(f"{LOG_FILE} is damaged. Rename it, then run Mission Log again.")
        return
# mission_log.py
# Mission Log: keep dated notes about your missions.
# Entries are saved in mission_log.json, in the same folder.

import json
from datetime import date

LOG_FILE = "mission_log.json"


def load_entries():
    """Read saved entries, or start an empty list on the first run."""
    try:
        with open(LOG_FILE) as f:
            return json.load(f)
    except FileNotFoundError:
        return []


def save_entries(entries):
    """Write every entry to the JSON file."""
    with open(LOG_FILE, "w") as f:
        json.dump(entries, f, indent=2)


def make_entry(text, tag, day):
    """Build one entry as a dictionary."""
    return {"date": day, "tag": tag, "text": text}


def format_entry(number, entry):
    """One entry as a line of text."""
    return f"{number}. {entry['date']} [{entry['tag']}] {entry['text']}"


def search_entries(entries, word):
    """Entries whose text or tag contains word, ignoring capitals."""
    word = word.lower()
    found = []
    for entry in entries:
        if word in entry["text"].lower() or word in entry["tag"]:
            found.append(entry)
    return found


def count_tags(entries):
    """A dictionary of how many entries have each tag."""
    counts = {}
    for entry in entries:
        tag = entry["tag"]
        counts[tag] = counts.get(tag, 0) + 1
    return counts


def add_entry(entries):
    """Ask for a new entry, add it to the list and save."""
    text = input("What happened? ").strip()
    if text == "":
        print("Nothing typed, so nothing saved.")
        return
    tag = input("Tag (build, test, idea): ").strip().lower()
    if tag == "":
        tag = "note"
    entries.append(make_entry(text, tag, date.today().isoformat()))
    save_entries(entries)
    print(f"Saved entry {len(entries)}.")


def show_entries(entries):
    """Print every entry with its number."""
    if len(entries) == 0:
        print("No entries yet.")
        return
    for number, entry in enumerate(entries, start=1):
        print(format_entry(number, entry))


def run_search(entries):
    """Ask for a word and print the matching entries."""
    word = input("Search for: ").strip()
    found = search_entries(entries, word)
    for number, entry in enumerate(found, start=1):
        print(format_entry(number, entry))
    print(f"Matches found: {len(found)}")


def show_stats(entries):
    """Print the number of entries and a count for each tag."""
    print(f"Entries in total: {len(entries)}")
    for tag, count in count_tags(entries).items():
        print(f"  {tag}: {count}")


def main():
    """Load the log, then show the menu until the user quits."""
    entries = load_entries()
    print(f"Mission Log ready. Entries loaded: {len(entries)}")
    while True:
        print()
        print("1 Add   2 List   3 Search   4 Stats   5 Quit")
        choice = input("Choose 1 to 5: ").strip()
        if choice == "1":
            add_entry(entries)
        elif choice == "2":
            show_entries(entries)
        elif choice == "3":
            run_search(entries)
        elif choice == "4":
            show_stats(entries)
        elif choice == "5":
            print("Log saved. See you next mission!")
            break
        else:
            print("Please type a number from 1 to 5.")


if __name__ == "__main__":
    main()

Here is a damaged mission_log.json. Before the fix, Mission Log stops with a traceback like this (shortened):

[
  {"date": "2026-04-10", "tag": "build"
Traceback (most recent call last):
  File "mission_log.py", line 118, in <module>
    main()
  File "mission_log.py", line 96, in main
    entries = load_entries()
  File "mission_log.py", line 15, in load_entries
    return json.load(f)
  (more lines from inside the json module)
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 3 column 1 (char 42)

After the fix, it shows one clear message and leaves the file untouched:

mission_log.json is damaged. Rename it, then run Mission Log again.
EDGE CASES
  • Read the question.
  • Tap your answer.
Why not just start an empty list when the file is damaged?
At your computer
1. Replace run_search and load_entries with the new versions, and add the three lines in main.
2. Run Mission Log. Press Enter at the search prompt and check the new message.
3. Save a copy of your log first: in your file browser, copy mission_log.json to log_backup.json.
4. Damage mission_log.json in IDLE by deleting its last line. Save it and run Mission Log.
5. Restore your log: delete the damaged mission_log.json and rename log_backup.json back to mission_log.json.
6. Run test_mission_log.py to check nothing else broke.

Mission Log now handles the cases real users will find. Tomorrow you will polish it and save your first snapshot.

← Monday