Contact Book

Python Projects — Contact Book
Python Projects (Beginner)
Course 4 · Chapter 6 · Contact Book

📇 Contact Book

Chapter 4's to-do app only ever added and completed tasks. This chapter builds the full set: Create, Read, Update, and Delete — the four operations almost every real data-backed app needs — applied to a simple JSON-backed 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.

⚠ Deleting Shifts Every Later Index Down by One

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.

🚀 Extend This Project

Try these on your own:

  • Give each contact a persistent id instead of relying on list position, fixing this chapter's own warn-box concern properly.
  • Add a CSV export option using the csv module (Course 2, Chapter 7) so contacts can be opened in a spreadsheet.
  • Validate that email contains 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.