Contact Book
๐ Contact Book
๐ฎ What We're Building
A contact book storing name, phone, and email per contact, supporting adding new contacts, searching/listing them, editing an existing contact's details, and deleting one โ all persisted to a JSON file between runs.
Step 1: Representing a Contact
contact = {"name": "Ada Lovelace", "phone": "555-0100", "email": "ada@example.com"}
Same "plain dict, no class" approach as Chapter 4's tasks โ a contact is just three related pieces of string data (Course 1, Chapters 5 and 7).
Step 2: Create โ Adding a Contact
def add_contact(contacts, name, phone, email): contacts.append({"name": name, "phone": phone, "email": email})
Step 3: Read โ Listing and Searching
def list_contacts(contacts): for i, c in enumerate(contacts, start=1): print(f"{i}. {c['name']} โ {c['phone']} โ {c['email']}") def search_contacts(contacts, query): query = query.lower() return [c for c in contacts if query in c["name"].lower()]
search_contacts reuses a list comprehension (Course 1/2) with a substring in check โ .lower() on both sides makes the search case-insensitive, so searching "ada" still finds "Ada Lovelace".
Step 4: Update โ Editing a Contact
def update_contact(contacts, index, field, new_value): if 0 <= index < len(contacts): contacts[index][field] = new_value else: print("Invalid contact number.")
The same bounds-checked guard clause from Chapter 4's complete_task. contacts[index][field] = new_value reassigns one key on the target contact's dict directly โ field can be "name", "phone", or "email", letting one function handle updating any of the three.
Step 5: Delete โ Removing a Contact
def delete_contact(contacts, index): if 0 <= index < len(contacts): del contacts[index] else: print("Invalid contact number.")
del contacts[index] reuses del from Course 1, Chapter 7's dictionary coverage โ it works on list items too, removing exactly the one element at that position and shifting the rest down.
After delete_contact(contacts, 2), whatever was previously contact #4 is now contact #3 โ every index after the deleted one shifts down. If the menu loop (Step 6) ever displayed a list and then asked for a number to act on before re-listing, a stale index could now point at the wrong contact. Always re-list before acting on an index, or (as Chapter 4's own extension suggested) give each contact a persistent ID that never changes regardless of deletions elsewhere in the list.
Step 6: Tying It Together โ Load, Save, and the Menu Loop
import json from pathlib import Path FILE = Path("contacts.json") def load_contacts(): if not FILE.exists(): return [] return json.loads(FILE.read_text()) def save_contacts(contacts): FILE.write_text(json.dumps(contacts, indent=2))
Identical load/save shape to Chapter 4's to-do app โ this pattern (a Path, a load_ function returning [] if the file doesn't exist yet, a save_ function writing the whole list back) is worth recognizing as reusable across almost any small JSON-backed CLI tool, not something unique to contacts or tasks specifically.
๐ The Complete Contact Book
import json from pathlib import Path FILE = Path("contacts.json") def load_contacts(): if not FILE.exists(): return [] return json.loads(FILE.read_text()) def save_contacts(contacts): FILE.write_text(json.dumps(contacts, indent=2)) def add_contact(contacts, name, phone, email): contacts.append({"name": name, "phone": phone, "email": email}) def list_contacts(contacts): for i, c in enumerate(contacts, start=1): print(f"{i}. {c['name']} โ {c['phone']} โ {c['email']}") def search_contacts(contacts, query): query = query.lower() return [c for c in contacts if query in c["name"].lower()] def update_contact(contacts, index, field, new_value): if 0 <= index < len(contacts): contacts[index][field] = new_value else: print("Invalid contact number.") def delete_contact(contacts, index): if 0 <= index < len(contacts): del contacts[index] else: print("Invalid contact number.") contacts = load_contacts() while True: print("\n1. Add 2. List 3. Search 4. Update 5. Delete 6. Quit") choice = input("Choose an option: ") if choice == "1": name = input("Name: ") phone = input("Phone: ") email = input("Email: ") add_contact(contacts, name, phone, email) save_contacts(contacts) elif choice == "2": list_contacts(contacts) elif choice == "3": query = input("Search for: ") for c in search_contacts(contacts, query): print(f"{c['name']} โ {c['phone']} โ {c['email']}") elif choice == "4": list_contacts(contacts) num = int(input("Contact number to update: ")) field = input("Field to update (name/phone/email): ") new_value = input("New value: ") update_contact(contacts, num - 1, field, new_value) save_contacts(contacts) elif choice == "5": list_contacts(contacts) num = int(input("Contact number to delete: ")) delete_contact(contacts, num - 1) save_contacts(contacts) elif choice == "6": print("Goodbye!") break else: print("Not a valid option, try again.")
Note that options 4 and 5 both call list_contacts(contacts) first, before asking for a number โ directly applying this chapter's own warn-box advice, so the number the user types always matches what they're currently looking at.
Create
add_contact() โ appends a new dict to the list.
Read
list_contacts() / search_contacts() โ display or filter.
Update
update_contact() โ reassigns one field on an existing dict.
Delete
delete_contact() โ del by index, shifting later items down.
Try these on your own:
- Give each contact a persistent
idinstead of relying on list position, fixing this chapter's own warn-box concern properly. - Add a CSV export option using the
csvmodule (Course 2, Chapter 7) so contacts can be opened in a spreadsheet. - Validate that
emailcontains an@symbol before accepting it, printing an error and re-asking otherwise. - Prevent exact duplicate names from being added, warning the user and asking for confirmation first.
๐ฏ What's Next
Next chapter: Simple Web Scraper โ introducing an external library, requests and BeautifulSoup.