1 The Problem
We want a note-taking app: write a note, it is saved with a timestamp and an ID; list all notes; delete one by ID. Everything persists in a JSON file. It teaches managing a store of structured records with unique IDs and timestamps — the shape of nearly every real app’s data.
2 How to Think About It
Think about what each note needs, before any code:
3 The Build — explained part by part
Here is the complete note app. Each part is explained below.
import json
import os
from datetime import datetime
FILE = "notes.json"
def load():
if os.path.exists(FILE):
with open(FILE) as f:
return json.load(f)
return []
def save(notes):
with open(FILE, "w") as f:
json.dump(notes, f, indent=2)
def add_note(notes, text):
# Next id is one more than the highest existing id (or 1 if empty).
next_id = max((n["id"] for n in notes), default=0) + 1
notes.append({
"id": next_id,
"text": text,
"created": datetime.now().isoformat(timespec="seconds"),
})
return notes
def delete_note(notes, note_id):
return [n for n in notes if n["id"] != note_id]
if __name__ == "__main__":
notes = load()
while True:
choice = input("\n1. New 2. List 3. Delete 4. Quit: ")
if choice == "1":
notes = add_note(notes, input("Note: "))
save(notes)
elif choice == "2":
for n in notes:
print(f"[{n['id']}] {n['created']} {n['text']}")
elif choice == "3":
notes = delete_note(notes, int(input("Delete id: ")))
save(notes)
elif choice == "4":
break
max((n["id"] for n in notes), default=0) + 1 — work out the next ID: one more than the biggest existing ID, or 1 if there are none.
default=0 handles the empty case so max does not error.datetime.now().isoformat(timespec="seconds") — the current time as clean text like
2026-06-24T17:30:00. Storing as text means JSON can save it.add_note builds the record (id + text + created) and appends it.
delete_note — returns a new list with every note except the one whose ID matches. Filtering to remove is cleaner than searching-then-deleting.
json.dump(notes, f, indent=2) —
indent=2 saves the file nicely formatted, so you can read it by hand.
len(notes) + 1 for the next ID — after deletions, this reuses IDs and causes clashes.max on an empty list — it errors with no default.default=0 so the first note still works.datetime object — JSON cannot save it..isoformat() first.4 Test & Prove Each Part
We test creating and deleting notes — the ID logic and the filtering — with known data.
from datetime import datetime
def add_note(notes, text):
next_id = max((n["id"] for n in notes), default=0) + 1
notes.append({"id": next_id, "text": text, "created": "now"})
return notes
def delete_note(notes, note_id):
return [n for n in notes if n["id"] != note_id]
def test_first_id():
"""The first note gets ID 1."""
notes = add_note([], "hello")
assert notes[0]["id"] == 1
def test_increasing_ids():
"""A second note gets ID 2."""
notes = add_note(add_note([], "a"), "b")
assert notes[1]["id"] == 2
def test_delete():
"""Deleting ID 1 leaves only the others."""
notes = add_note(add_note([], "a"), "b")
remaining = delete_note(notes, 1)
assert len(remaining) == 1
assert remaining[0]["id"] == 2
Run with pytest -v. The ID and delete logic live in functions, so we can test them directly. The increasing-IDs test proves new notes never reuse an old ID.
5 The Interface
What it expects
Note: Buy groceries after workWhat it returns
[1] 2026-06-24T17:30:00 Buy groceries after work6 Run It & Automate It
python3 notes.pyCreate notes, list them, delete by ID. Saved in
notes.json.Jenkins runs the tests automatically — each line explained below.
1. New 2. List 3. Delete 4. Quit: 1
Note: Buy groceries
1. New 2. List 3. Delete 4. Quit: 2
[1] 2026-06-24T17:30:00 Buy groceriesTypeError: Object of type datetime is not JSON serializable..isoformat(), not a datetime object.max(ids) + 1 instead.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Edit a note. Change the text of an existing note by ID. (Teaches: updating records.)
- Search. Find notes containing a word. (Teaches: filtering by text.)
- Tags. Add tags and list by tag. (Teaches: richer records.)