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.