1 The Problem
We want a contact book: add people with a name and phone number, search for one by name, list them all, and save everything to a file. It teaches storing structured records (each contact has fields) and searching through them — the foundation of any database-backed app.
2 How to Think About It
Think about how to store a contact, before any code:
3 The Build — explained part by part
Here is the complete contact book. Each part is explained below.
import json
import os
FILE = "contacts.json"
def load():
if os.path.exists(FILE):
with open(FILE) as f:
return json.load(f)
return []
def save(contacts):
with open(FILE, "w") as f:
json.dump(contacts, f)
contacts = load()
while True:
print("\n1. Add 2. Search 3. List 4. Quit")
choice = input("Choose: ")
if choice == "1":
name = input("Name: ")
phone = input("Phone: ")
contacts.append({"name": name, "phone": phone})
save(contacts)
elif choice == "2":
term = input("Search name: ").lower()
matches = [c for c in contacts if term in c["name"].lower()]
for c in matches:
print(f"{c['name']}: {c['phone']}")
elif choice == "3":
for c in contacts:
print(f"{c['name']}: {c['phone']}")
elif choice == "4":
break
contacts.append({...}) — add the new contact bundle to the list.
[c for c in contacts if term in c["name"].lower()] — the search: walk through every contact and keep the ones whose name contains the search term. Lowercasing both sides makes search case-insensitive.
c["name"], c["phone"] — pull a field out of a contact by its label.
load() / save() — the same JSON persistence as before, so contacts survive between runs.
== instead of in — then partial names do not match.in for partial matches: term in name..lower() on both sides — case-sensitive search frustrates users.4 Test & Prove Each Part
We test adding and searching contacts directly, with known data.
def add(contacts, name, phone):
contacts.append({"name": name, "phone": phone})
return contacts
def search(contacts, term):
term = term.lower()
return [c for c in contacts if term in c["name"].lower()]
def test_add():
"""Adding stores name and phone."""
result = add([], "Alice", "123")
assert result == [{"name": "Alice", "phone": "123"}]
def test_search_partial():
"""Search finds by part of the name."""
contacts = [{"name": "Alice", "phone": "1"}, {"name": "Bob", "phone": "2"}]
assert len(search(contacts, "ali")) == 1
def test_search_case_insensitive():
"""Search ignores capitalisation."""
contacts = [{"name": "Alice", "phone": "1"}]
assert len(search(contacts, "ALICE")) == 1
Run with pytest -v. The add and search functions hold the logic, so tests check them with known contacts. The case-insensitive test proves search is forgiving.
5 The Interface
What it expects
Choose: 1
Name: Alice
Phone: 555-1234What it returns
Alice: 555-12346 Run It & Automate It
python3 contacts.pyAdd contacts, search, and list them — they are saved in
contacts.json.Jenkins runs the tests automatically — each line explained below.
1. Add 2. Search 3. List 4. Quit
Choose: 1
Name: Alice
Phone: 555-1234
Choose: 2
Search name: ali
Alice: 555-1234KeyError: 'name'.{"name":..., "phone":...}.save(contacts) after adding.// 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 and delete. Add options to change or remove a contact. (Teaches: finding and updating records.)
- More fields. Add email and address. (Teaches: richer records.)
- Sort by name. List contacts alphabetically. (Teaches: sorting dictionaries.)