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.
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)}") 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.
Mission Log now handles the cases real users will find. Tomorrow you will polish it and save your first snapshot.