Lecture 07: Capstone Project 1 - Building Your Personal Contact Book

Lecture 07: Capstone Project 1 - Building Your Personal Contact Book

Welcome to Lecture 07! Over the past six lectures, you’ve learned a tremendous amount about Python programming, from basic variables and printing, to functions, loops, conditional logic, and even how to make your programs remember data using files with lists, dictionaries, and the JSON format.

Today’s Goal: Tying It All Together
In this capstone project lecture, our main goal is to build a complete, useful command-line application: Your Personal Contact Book. This project will require you to apply almost everything you’ve learned so far. We’ll design it, build it step-by-step, and by the end, you’ll have a functional program that can:

  • Add new contacts (name, phone, email, etc.).
  • View all saved contacts.
  • Search for specific contacts.
  • Delete contacts.
  • Save all contact information to a file so it persists even after you close the program!

This project is designed to consolidate your knowledge and give you the satisfaction of creating a more substantial piece of software. Let’s get started!

Recap of Key Concepts We’ll Be Using

This project will be a fantastic way to practice and see in action many of the concepts we’ve covered:

  • Variables: To store individual pieces of data like a contact’s name or phone number.
  • Strings: For all textual information (names, emails, addresses). We’ll use string methods like .lower() and .strip().
  • Lists: The main way we’ll store our collection of contacts. Each item in the list will represent one contact.
  • Dictionaries: Perfect for storing the details of a single contact. Each contact will be a dictionary with keys like “name”, “phone”, “email”.
  • Functions (def): To break our program into manageable, reusable, and logical pieces (e.g., a function to add a contact, a function to view contacts).
  • while Loops: For the main application loop, keeping the menu running until the user decides to quit.
  • for Loops: For iterating through our list of contacts (e.g., to display them or search for one). We’ll use enumerate() too!
  • if/elif/else Conditionals: To process user menu choices and for other logical decisions within our functions.
  • File I/O (Input/Output):
    • pathlib.Path: For modern and robust handling of file paths.
    • json module: To save our list of contact dictionaries to a file (contacts.json) in a structured way and load it back.
    • The with my_path.open(...) as f: pattern for safe file handling.
  • try-except Blocks: For error handling, especially when dealing with file operations (like FileNotFoundError or json.JSONDecodeError) or user input conversion (like ensuring a choice is a number if needed, though our menu will be string-based).
  • User Input (input()): To get information from the user.
  • Formatted Output (print() with f-strings): To display information clearly.

This project is like a mini-summit of your Python learning so far!

Planning Your Contact Book (An Interactive Design Discussion)

Before we write a single line of Python for the application itself, let’s plan it out. Good planning makes the coding process much smoother.

1. What Information Makes Up a Single Contact?
If you think about a contact in your phone or email, what details do you usually store?

  • Name: Definitely essential! (e.g., “Priya Sharma”)
  • Phone Number: Also very important. (e.g., “9876543210”)
  • Email Address: Very common. (e.g., “priya.sharma@example.com”)
  • Address: Could be useful (e.g., “123 Green Park, Delhi”).
  • Notes/Birthday/Company…?: We could add many more fields!

For our first version, let’s keep it manageable but useful. We’ll aim for:

  • Name (Essential)
  • Phone Number (Essential)
  • Email Address (Optional)
  • Address (Optional)

2. How Should We Store a Single Contact’s Details in Python?
We have a few pieces of information for each contact (name, phone, email, address). We need a way to group these related pieces of data together.

  • Could we use a list? Maybe ["Priya Sharma", "9876543210", "priya@example.com"]. But then we have to remember that index 0 is the name, index 1 is the phone, etc. This can get confusing.
  • What about a dictionary? This seems perfect! We can use descriptive keys:
    contact_priya = {
        "name": "Priya Sharma",
        "phone": "9876543210",
        "email": "priya.sharma@example.com",
        "address": "123 Green Park, Delhi"
    }
    
    This is very clear! Each piece of information is labeled by its key. A dictionary is the way to go for storing individual contact details.

3. How Should We Store Many Contacts?
Our contact book will hold many contacts. If each contact is a dictionary, how do we store a collection of these dictionaries?

  • A list of dictionaries is the natural choice here!
    all_contacts = [
        {
            "name": "Priya Sharma", "phone": "9876543210",
            "email": "priya@example.com", "address": "123 Green Park, Delhi"
        },
        {
            "name": "Rohan Mehra", "phone": "9988776655",
            "email": "rohan.m@example.com", "address": "456 Blue Avenue, Mumbai"
        },
        # ... more contact dictionaries ...
    ]
    
    This structure allows us to have an ordered collection of our contacts, and each contact itself is a well-structured dictionary.

4. What Actions (Features) Do We Want Our Application to Perform?
Let’s list the main things a user should be able to do:

  • Add a New Contact: Enter details for a new person and add them to our list.
  • View All Contacts: Display all the contacts currently stored, in a readable format.
  • Search for a Contact: Find a contact by searching for their name (or maybe phone number later).
  • Delete a Contact: Remove a contact from the book.
  • Exit: Quit the application.

Each of these actions will likely become a function in our Python code to keep things organized.

5. How Do We Make the Data Permanent? (Persistence)
As we discussed in Lecture 06, if our all_contacts list is just in memory, it will be lost when the program ends. We need to save it to a file.

  • File Format: Given that we’re storing a list of dictionaries, JSON is an excellent choice. It maps directly to Python’s list/dictionary structure and is human-readable.
  • Filename: We’ll choose a name like contacts.json. We’ll use pathlib.Path to manage this file path.
  • When to Load? When the application starts, it should try to load existing contacts from contacts.json. If the file doesn’t exist (e.g., first time running), it will start with an empty list of contacts.
  • When to Save? After any operation that changes the list of contacts (adding a new contact, deleting a contact), we should save the updated list back to contacts.json. This ensures data is always up-to-date. (Alternatively, some apps only save on exit, but saving on change is often safer).

This planning gives us a solid blueprint for our application!

Designing Our Functions

Let’s outline the purpose, parameters (inputs), and potential return values for each main function. This helps us think about how they’ll interact.

  • display_menu()

    • Purpose: Prints the user menu with options (Add, View, Search, Delete, Exit).
    • Parameters: None.
    • Returns: Nothing.
  • add_contact(contacts_list: list)

    • Purpose: Prompts the user to enter details for a new contact, creates a contact dictionary, and appends it to the contacts_list.
    • Parameters: contacts_list (the main list of contact dictionaries that this function will modify).
    • Returns: Maybe True if a contact was successfully added, False otherwise (e.g., if the user didn’t provide essential info). This can help the main loop decide if a save is needed.
  • view_contacts(contacts_list: list)

    • Purpose: Displays all contacts in the contacts_list in a nicely formatted way. Should handle the case where the list is empty.
    • Parameters: contacts_list.
    • Returns: Nothing.
  • search_contact(contacts_list: list)

    • Purpose: Prompts the user for a search term (e.g., a name or part of a name). It then iterates through the contacts_list, finds any matching contacts (case-insensitive search is good), and displays them.
    • Parameters: contacts_list.
    • Returns: Could return a new list containing only the found contacts, or perhaps just print them directly. Let’s aim for printing directly for now to keep it simpler, but also return a list of found contacts for potential use by delete_contact.
  • delete_contact(contacts_list: list)

    • Purpose: Allows the user to remove a contact. This is often the trickiest part.
    • Parameters: contacts_list.
    • Steps it might take:
      1. Prompt for a search term to find the contact to delete (similar to search_contact).
      2. Display matching contacts with numbers (e.g., “1. Priya Sharma”, “2. Priya Kulkarni”).
      3. Ask the user to confirm which numbered contact they want to delete.
      4. Validate the user’s numerical input.
      5. Remove the chosen contact dictionary from the contacts_list (using pop() with the correct index).
    • Returns: True if deletion was successful, False otherwise.
  • load_contacts_from_file(filepath_obj: Path)

    • Purpose: Loads the list of contact dictionaries from the specified JSON file.
    • Parameters: filepath_obj (a pathlib.Path object representing the data file).
    • Returns: A list of contact dictionaries. If the file doesn’t exist or is invalid, it returns an empty list [].
    • Error Handling: Needs try-except for FileNotFoundError and json.JSONDecodeError.
  • save_contacts_to_file(contacts_list: list, filepath_obj: Path)

    • Purpose: Saves the current contacts_list (of dictionaries) to the specified JSON file.
    • Parameters: contacts_list, filepath_obj.
    • Returns: Nothing (but might print a success/error message).
    • Uses json.dump() with indent=4.
  • run_contact_book_app() (Main application function)

    • Purpose: Contains the main application loop, loads data on start, shows menu, gets user choice, calls other functions, and ensures data is saved before exiting.
    • Parameters: None.
    • Returns: Nothing.

This function design provides a good separation of concerns.

Step-by-Step Implementation Guidance

We won’t write every single line of Python here in the markdown repeatedly, but let’s outline the key implementation steps and show important snippets. You’ll combine these into your contact_book_app.py file.

1. Setup (contact_book_app.py):
Create your Python file. Start with necessary imports and constants:

import json
from pathlib import Path # For modern path handling

# Define the path to the contacts data file using pathlib
CONTACTS_FILEPATH = Path("contacts.json")

2. Core Data Structure (Conceptual):
Remember, your main data will be a list of dictionaries. This list will be loaded from the file at the start of your main application function and passed to other functions as needed. For example:
all_my_contacts = [] (This will be initialized by load_contacts_from_file).

3. File Handling Functions First:
It’s often a good idea to implement load_contacts_from_file and save_contacts_to_file early on.

  • load_contacts_from_file(filepath: Path):
    # (As defined in the previous section - using Path.exists(), Path.open(), json.load(), try-except)
    # Remember it should return [] if file not found or error.
    
  • save_contacts_to_file(contacts_list: list, filepath: Path):
    # (As defined previously - using Path.open('w'), json.dump(..., indent=4), try-except)
    

You can test these! Manually create a simple contacts.json file like:

[
    {"name": "Test User", "phone": "123", "email": "test@example.com", "address": "Test Address"}
]

Then, in your Python script, temporarily add:

# test_contacts = load_contacts_from_file(CONTACTS_FILEPATH)
# print(f"Loaded for test: {test_contacts}")
# if test_contacts:
#     test_contacts[0]["name"] = "Test User Updated" # Make a change
#     save_tasks_to_file(test_contacts, CONTACTS_FILEPATH) # Save it back

This helps confirm your file I/O is working before building the whole app.

4. Main Application Structure (run_contact_book_app() function):
This function will orchestrate everything.

def run_contact_book_app():
    # Load contacts from file at the start
    contacts = load_contacts_from_file(CONTACTS_FILEPATH)

    while True:
        display_menu() # Your function to print menu options
        choice = input("Enter your choice (1-5): ").strip()

        if choice == '1':
            if add_contact(contacts): # Pass the list to be modified
                save_contacts_to_file(contacts, CONTACTS_FILEPATH)
        elif choice == '2':
            view_contacts(contacts)
        elif choice == '3':
            search_contact(contacts) # Or get results and pass to delete
        elif choice == '4':
            if delete_contact(contacts): # Pass the list to be modified
                save_contacts_to_file(contacts, CONTACTS_FILEPATH)
        elif choice == '5':
            print("Saving contacts and exiting Contact Book. Goodbye!")
            save_contacts_to_file(contacts, CONTACTS_FILEPATH) # Ensure final save
            break
        else:
            print("Invalid choice. Please select a valid option from the menu.")

        input("\nPress Enter to continue...") # Pause for user

5. Implementing Each Feature Function:

  • add_contact(contacts_list):

    • Prompt for name, phone, email, address.
    • Input Validation (Basic): At least name and phone should not be empty. You can use if not name.strip(): print("Name cannot be empty."); return False.
    • Create the dictionary: new_contact = {"name": name, "phone": phone, ...}.
    • Append: contacts_list.append(new_contact).
    • Print success message. Return True.
  • view_contacts(contacts_list):

    • Check if not contacts_list: print("Your contact book is empty.").
    • Else, loop with enumerate(contacts_list, start=1) to get a display number.
    • For each contact_dict in the list, print its details neatly formatted.
    # Inside view_contacts:
    # for index, contact in enumerate(contacts_list, start=1):
    #     print(f"\n--- Contact #{index} ---")
    #     print(f"  Name: {contact.get('name', 'N/A')}")
    #     print(f"  Phone: {contact.get('phone', 'N/A')}")
    #     print(f"  Email: {contact.get('email', 'N/A')}")
    #     print(f"  Address: {contact.get('address', 'N/A')}")
    

    Using .get(key, 'N/A') is a nice way to display “N/A” if a field is missing for some contact, preventing KeyErrors.

  • search_contact(contacts_list):

    • Get search term from user: search_term = input("Enter name (or part of name) to search: ").lower().strip().
    • Initialize an empty list found_contacts = [].
    • Loop for contact in contacts_list:.
    • Inside the loop, check if search_term in contact.get('name', '').lower(). (Using .get('name', '') avoids error if a contact somehow has no name).
    • If a match is found, append the contact dictionary to found_contacts.
    • After the loop, if found_contacts is not empty, display them (perhaps using view_contacts logic or a simpler version).
    • If empty, print “No contacts found matching your search.”
    • Return found_contacts (this can be useful for delete_contact).
  • delete_contact(contacts_list):

    • Get search term from user, similar to search_contact.
    • Call a modified search function or implement search logic here to find potential matches. Let’s say it returns a list matches_to_delete.
    • If not matches_to_delete, print “No contact found to delete.” and return False.
    • If there’s one match: contact_to_delete = matches_to_delete[0]. Ask for confirmation: “Are you sure you want to delete {contact_to_delete[‘name’]}? (yes/no)”. If yes, contacts_list.remove(contact_to_delete) (or use pop if you have its index). Print success. Return True.
    • If multiple matches: Print them with numbers (like view_contacts). Ask user to “Enter the number of the contact to delete:”. Validate input. Convert to index. pop() the contact. Print success. Return True.
    • If user says no to confirmation or enters invalid number, return False.

This step-by-step approach, building and testing each function, will make the development of the full application much more manageable.

Full Code Presentation (contact_book_app.py)

Here is the complete, well-commented Python code for our Personal Contact Book application.

# examples/contact_book/contact_book_app.py
import json
from pathlib import Path

# Define the path to the contacts data file using pathlib
CONTACTS_FILEPATH = Path("contacts_data.json")

# --- File Operations ---
def load_contacts_from_file(filepath: Path) -> list:
    """
    Loads contacts from a JSON file using pathlib.Path.
    Returns a list of contact dictionaries.
    If the file doesn't exist or is invalid, returns an empty list.
    """
    try:
        if filepath.exists() and filepath.is_file():
            with filepath.open(mode='r', encoding='utf-8') as f:
                contacts_data = json.load(f)
                # Basic validation: Ensure we loaded a list.
                if not isinstance(contacts_data, list):
                    print(f"Warning: Data in {filepath} is not in list format. Starting fresh.")
                    return []
                # print(f"Contacts loaded successfully from {filepath}") # Optional debug
                return contacts_data
        else:
            # print(f"Info: No contacts file found at {filepath}. Starting with an empty list.") # Optional
            return []
    except json.JSONDecodeError:
        print(f"Error: Could not decode JSON from '{filepath}'. File might be corrupted. Starting fresh.")
        return []
    except Exception as e:
        print(f"An unexpected error occurred while trying to load contacts: {e}")
        return []

def save_contacts_to_file(contacts_list: list, filepath: Path):
    """
    Saves the given list of contact dictionaries to a JSON file using pathlib.Path.
    Overwrites the file if it already exists.
    """
    try:
        with filepath.open(mode='w', encoding='utf-8') as f:
            json.dump(contacts_list, f, indent=4)
        # print(f"Contacts successfully saved to '{filepath}'") # Optional debug
    except Exception as e:
        print(f"An error occurred while trying to save contacts to '{filepath}': {e}")

# --- Core Contact Book Functions ---
def display_menu():
    """Prints the main menu options to the user."""
    print("\n===== Personal Contact Book Menu =====")
    print("1. Add a new contact")
    print("2. View all contacts")
    print("3. Search for a contact")
    print("4. Delete a contact")
    print("5. Exit")
    print("====================================")

def add_contact(contacts_list: list) -> bool:
    """
    Prompts user for new contact details, creates a contact dictionary,
    and appends it to the contacts_list.
    Returns True if contact was added, False otherwise.
    """
    print("\n--- Add New Contact ---")
    name = input("Enter name: ").strip()
    phone = input("Enter phone number: ").strip()
    email = input("Enter email address (optional): ").strip()
    address = input("Enter address (optional): ").strip()

    if not name or not phone: # Name and Phone are mandatory
        print("Error: Name and Phone number cannot be empty.")
        return False

    new_contact = {
        "name": name,
        "phone": phone,
        "email": email if email else "N/A", # Store N/A if empty
        "address": address if address else "N/A"
    }
    contacts_list.append(new_contact)
    print(f"Contact for '{name}' added successfully!")
    return True

def view_contacts(contacts_list: list):
    """Displays all contacts in the contacts_list in a formatted way."""
    print("\n--- All Contacts ---")
    if not contacts_list:
        print("Your contact book is empty.")
    else:
        for index, contact in enumerate(contacts_list, start=1):
            print(f"\nContact #{index}:")
            print(f"  Name:    {contact.get('name', 'N/A')}")
            print(f"  Phone:   {contact.get('phone', 'N/A')}")
            print(f"  Email:   {contact.get('email', 'N/A')}")
            print(f"  Address: {contact.get('address', 'N/A')}")
    print("--------------------")

def search_contact(contacts_list: list) -> list:
    """
    Prompts user for a search term and displays matching contacts.
    Returns a list of found contact dictionaries.
    """
    print("\n--- Search Contacts ---")
    if not contacts_list:
        print("Your contact book is empty. Nothing to search.")
        return []

    search_term = input("Enter name or part of name to search for: ").lower().strip()
    if not search_term:
        print("Search term cannot be empty.")
        return []

    found_contacts = []
    for contact in contacts_list:
        # Case-insensitive search in name
        if search_term in contact.get('name', '').lower():
            found_contacts.append(contact)

    if not found_contacts:
        print(f"No contacts found matching '{search_term}'.")
    else:
        print(f"Found {len(found_contacts)} contact(s) matching '{search_term}':")
        # Re-use view_contacts to display them consistently, but with original numbering
        # For simplicity here, just print basic info
        for i, contact in enumerate(found_contacts, start=1):
            print(f"  {i}. Name: {contact.get('name')}, Phone: {contact.get('phone')}")
    return found_contacts

def delete_contact(contacts_list: list) -> bool:
    """
    Allows user to search for a contact and then delete one from the list.
    Returns True if a contact was deleted, False otherwise.
    """
    print("\n--- Delete Contact ---")
    if not contacts_list:
        print("Your contact book is empty. Nothing to delete.")
        return False

    search_term = input("Enter name (or part of name) of the contact to delete: ").lower().strip()
    if not search_term:
        print("Search term cannot be empty.")
        return False

    matches = []
    for contact in contacts_list:
        if search_term in contact.get('name', '').lower():
            matches.append(contact)

    if not matches:
        print(f"No contacts found matching '{search_term}'.")
        return False

    print("Found the following matching contacts:")
    for index, contact in enumerate(matches, start=1):
        print(f"  {index}. {contact.get('name')} - {contact.get('phone')}")

    if len(matches) == 1:
        choice_str = "1" # Automatically select if only one match
    else:
        choice_str = input("Enter the number of the contact you want to delete: ")

    try:
        choice_num = int(choice_str)
        if 1 <= choice_num <= len(matches):
            contact_to_delete = matches[choice_num - 1] # Get the actual dict from matches

            # Confirm before deleting
            confirm = input(f"Are you sure you want to delete {contact_to_delete.get('name')}? (yes/no): ").lower()
            if confirm == 'yes':
                contacts_list.remove(contact_to_delete) # Remove the dictionary from the main list
                print(f"Contact '{contact_to_delete.get('name')}' deleted successfully.")
                return True
            else:
                print("Deletion cancelled.")
                return False
        else:
            print("Invalid selection number.")
            return False
    except ValueError:
        print("Invalid input. Please enter a number.")
        return False

# --- Main Application Logic ---
def run_contact_book_app():
    """Main function to run the Contact Book application loop."""
    # Load contacts from file at the very start
    contacts = load_contacts_from_file(CONTACTS_FILEPATH)

    while True:
        display_menu()
        choice = input("Enter your choice (1-5): ").strip()

        if choice == '1': # Add Contact
            if add_contact(contacts):
                save_contacts_to_file(contacts, CONTACTS_FILEPATH)
        elif choice == '2': # View All Contacts
            view_contacts(contacts)
        elif choice == '3': # Search Contact
            search_contact(contacts) # Search results are printed by the function
        elif choice == '4': # Delete Contact
            if delete_contact(contacts):
                save_contacts_to_file(contacts, CONTACTS_FILEPATH)
        elif choice == '5': # Exit
            print("\nSaving contacts and exiting Contact Book. Goodbye!")
            save_contacts_to_file(contacts, CONTACTS_FILEPATH) # Ensure data is saved before exiting
            break
        else:
            print("Invalid choice. Please select a valid option (1-5).")

        # Pause for the user to read messages before redisplaying the menu
        input("\nPress Enter to continue...")

# This standard Python line ensures run_contact_book_app() runs only
# when the script is executed directly (not when imported as a module).
if __name__ == "__main__":
    run_contact_book_app()

Understanding the Persistent Contact Book App: A Code Walkthrough

Let’s break down the contact_book_app.py script to understand how it uses all the concepts we’ve learned.

  1. Overall Structure:

    • Imports: import json is for working with JSON data (saving and loading). from pathlib import Path gives us the Path object for easier and more reliable file path management.
    • CONTACTS_FILEPATH = Path("contacts_data.json"): This global constant defines the name and location of our data file as a Path object. Using a constant makes it easy to change the filename later if needed, and Path makes it OS-independent.
    • The main data is intended to be managed within the run_contact_book_app function in a list called contacts. This list is populated by load_contacts_from_file and passed to other functions.
  2. Function display_menu():

    • This is a very simple function. Its sole responsibility is to print the menu options to the console, guiding the user on what actions they can perform. It takes no arguments and doesn’t return any value.
  3. Function load_contacts_from_file(filepath: Path):

    • Purpose: This function is crucial for persistence. It’s called once when the application starts to load any previously saved contacts from the JSON file.
    • filepath: Path Argument: It takes a Path object (our CONTACTS_FILEPATH) that points to the data file.
    • File Existence & Reading (try-except blocks):
      • if filepath.exists() and filepath.is_file():: It first checks if the specified path actually exists and is a file (not a directory). This prevents errors if the file is missing.
      • with filepath.open(mode='r', encoding='utf-8') as f:: If the file exists, it’s opened in read mode ('r'). encoding='utf-8' ensures wide character compatibility. The with statement guarantees the file is closed automatically.
      • contacts_data = json.load(f): This is the core of loading. json.load() reads the JSON formatted text from the file object f, parses it, and converts it into a Python data structure. Since we save our contacts as a JSON array of objects, this will return a Python list of dictionaries.
      • if not isinstance(contacts_data, list):: This is a safety check. If the JSON file was somehow manually changed and doesn’t contain a list at its root, this prevents the program from crashing later. It returns an empty list in such cases.
      • FileNotFoundError: If filepath.exists() is false, or if filepath.open() were called on a non-existent file directly, this error would occur. The current structure handles it by returning [] if filepath.exists() is false.
      • json.JSONDecodeError: If the file exists but its content is not valid JSON (e.g., it’s corrupted or has syntax errors), json.load(f) will raise this error. The except block catches this, prints a warning message, and returns an empty list, allowing the application to start fresh rather than crashing.
      • Generic Exception as e: Catches any other unexpected I/O errors.
    • Return Value: Returns the loaded list of contacts (which could be empty if the file wasn’t found or was invalid).
  4. Function save_contacts_to_file(contacts_list: list, filepath: Path):

    • Purpose: This function is called whenever the list of contacts is modified (a contact is added or deleted) to save the current state of contacts_list to the JSON file.
    • filepath.open(mode='w', encoding='utf-8') as f:: The file is opened in write mode ('w'). This means if the file already exists, its entire content will be overwritten with the new data. This is exactly what we want to reflect the current state of the contacts. If the file doesn’t exist, it will be created.
    • json.dump(contacts_list, f, indent=4): This function converts the Python contacts_list (which is a list of dictionaries) into JSON formatted text and writes it to the file object f.
      • indent=4: This is a very useful argument! It formats the JSON output in the file with an indentation of 4 spaces for each level of nesting. This makes the contacts_data.json file human-readable if you were to open it in a text editor. Without indent, the entire JSON data would usually be written on a single, long line.
    • Generic Exception as e: Catches potential errors during saving, like disk full or permission issues.
  5. Function add_contact(contacts_list: list):

    • Purpose: To gather information for a new contact from the user and add it to the main contacts_list.
    • It prompts the user for name, phone, email, and address using input(). .strip() is used to remove any accidental leading/trailing whitespace from the user’s input.
    • Validation: It checks if name or phone are empty (after stripping). If so, it prints an error and returns False (indicating the add operation failed).
    • Dictionary Creation: A new dictionary new_contact is created to store the details. If email or address were left empty by the user, it stores “N/A” as a placeholder.
    • contacts_list.append(new_contact): The newly created contact dictionary is added to the end of the contacts_list that was passed into the function.
    • It prints a success message and returns True.
  6. Function view_contacts(contacts_list: list):

    • Purpose: To display all the contacts stored in the contacts_list.
    • It first checks if not contacts_list: (a Pythonic way to see if the list is empty). If it is, an appropriate message is printed.
    • Otherwise, it loops through the contacts_list using enumerate(contacts_list, start=1). enumerate provides both a index (starting from 1 for user-friendly display) and the contact dictionary for each iteration.
    • Inside the loop, it prints the details of each contact dictionary in a formatted way. It uses contact.get('key', 'N/A') to safely access dictionary values; if a key (like ‘email’ or ‘address’) happens to be missing from a contact dictionary, it will print “N/A” instead of causing a KeyError.
  7. Function search_contact(contacts_list: list):

    • Purpose: Allows the user to find contacts by name.
    • It prompts for a search_term and converts it to lowercase using .lower() for case-insensitive searching.
    • It initializes an empty list found_contacts.
    • It loops through each contact in the contacts_list. Inside the loop, search_term in contact.get('name', '').lower() checks if the (lowercased) search term is present anywhere within the (lowercased) contact’s name. contact.get('name', '') ensures that if a contact dictionary somehow doesn’t have a ‘name’ key, it defaults to an empty string, preventing an error.
    • Matching contacts are appended to found_contacts.
    • After the loop, it displays the results or a “No contacts found” message.
    • It returns the found_contacts list, which might be useful for other functions (like a more advanced delete).
  8. Function delete_contact(contacts_list: list):

    • Purpose: To remove a contact from the contacts_list.
    • It first prompts for a search_term to find potential contacts to delete.
    • It builds a matches list similar to search_contact.
    • If no matches, it informs the user and returns False.
    • If matches are found, they are displayed with numbers.
    • If there’s only one match, it’s auto-selected. Otherwise, the user is asked to input the number of the contact to delete.
    • Confirmation: It asks the user to confirm the deletion with a “yes/no” input.
    • If confirmed, contacts_list.remove(contact_to_delete) is used. Note that contact_to_delete is the actual dictionary object retrieved from the matches list. list.remove() finds the first occurrence of that exact object in contacts_list and removes it.
    • It returns True on successful deletion, False otherwise (e.g., if deletion is cancelled or input is invalid).
  9. Function run_contact_book_app() (Main Application Logic):

    • Initial Load: contacts = load_contacts_from_file(CONTACTS_FILEPATH) is the very first important step. This populates the contacts list with data from contacts_data.json if it exists and is valid; otherwise, contacts starts as an empty list.
    • Main Loop (while True): Keeps the application running.
    • display_menu() shows the options.
    • User choice is read.
    • if/elif/else for Choices:
      • choice == '1' (Add): Calls add_contact(contacts). If it returns True (contact added), then save_contacts_to_file(contacts, CONTACTS_FILEPATH) is called immediately to make the new contact persistent.
      • choice == '2' (View): Calls view_contacts(contacts).
      • choice == '3' (Search): Calls search_contact(contacts).
      • choice == '4' (Delete): Calls delete_contact(contacts). If it returns True (contact deleted), then save_contacts_to_file(contacts, CONTACTS_FILEPATH) is called to persist the deletion.
      • choice == '5' (Exit): Prints a goodbye message, calls save_contacts_to_file one last time (as a safeguard, though ideally data is already saved), and then break terminates the loop, ending the program.
      • Else (Invalid choice): An error message is displayed.
    • input("\nPress Enter to continue..."): This pauses the program after each action, allowing the user to read any messages before the menu is displayed again. This improves user experience.
  10. The if __name__ == "__main__": Block:

    • This standard Python construct ensures that run_contact_book_app() (which starts the application) is called only when the script is executed directly (e.g., via python contact_book_app.py). It prevents run_contact_book_app() from running if, for example, you were to import functions from this script into another Python file as a module.

This detailed walkthrough shows how different functions collaborate, how data is passed (like the contacts list), and how file operations are integrated to create a complete and persistent application.

How to Run & Test

  1. Save: Save the code as contact_book_app.py in a directory like examples/contact_book/.
  2. Run: Open your terminal, navigate to that directory (cd examples/contact_book), and run python contact_book_app.py.
  3. Test: Add contacts, view them, search for them, delete some, then quit. Run the app again. Your changes should be loaded from contacts_data.json (which will be created in the same directory). Inspect this file to see the JSON structure.

Recap of Concepts Applied

This capstone project brought together many concepts you’ve learned:

  • Core Python: Variables, data types (strings, booleans).
  • Data Structures:
    • Lists: For the main collection of contact records.
    • Dictionaries: For storing the detailed information of each individual contact (using descriptive keys like “name”, “phone”).
  • Control Flow:
    • if/elif/else statements: For menu choice processing and conditional logic within functions.
    • for loops: For iterating through the list of contacts (e.g., in view_contacts, search_contact, delete_contact). We used enumerate for numbered display.
    • while True loop: For the main application menu, allowing repeated actions until the user quits.
  • Functions (def): Crucial for organizing the code into logical, reusable blocks (display_menu, add_contact, view_contacts, search_contact, delete_contact, load_contacts_from_file, save_contacts_to_file, run_contact_book_app). This made the code modular and easier to understand.
  • File I/O:
    • pathlib.Path: For creating and managing file paths in an object-oriented and OS-independent way.
    • Path.open() with with statement: For safe and automatic file handling.
    • File modes: 'r' (read) and 'w' (write).
    • json module:
      • json.load(): To read JSON data from a file and convert it into Python lists/dictionaries.
      • json.dump(): To write Python lists/dictionaries into a file in JSON format (using indent=4 for readability).
  • Error Handling (try-except):
    • FileNotFoundError: When trying to load contacts if the data file doesn’t exist yet.
    • json.JSONDecodeError: If the data file is corrupted or not valid JSON.
    • ValueError: When converting user input for menu choices or numbers for deletion if the input isn’t a valid integer.
  • User Input/Output: input() to get data from the user, and print() (using f-strings) to display information.
  • String Methods: .strip() to clean user input, .lower() for case-insensitive searching.
  • Program Structure: The if __name__ == "__main__": block to make the script runnable.

Homework & Further Challenges

Congratulations on conceptually building a complete application! Here are some ways you could extend or improve your Contact Book:

  1. Implement an “Edit Contact” Feature:

    • Add a new menu option “Edit an existing contact.”
    • This function would likely first need to find the contact to edit (you could reuse or adapt your search_contact logic or allow selection by number after viewing all).
    • Once the contact (dictionary) is found, prompt the user for which field they want to change (name, phone, email, address) and then the new value for that field.
    • Update the contact dictionary in the main contacts list.
    • Remember to call save_contacts_to_file() after any successful edit.
  2. Improve Search Functionality:

    • Currently, search is likely by name only. Allow the user to choose if they want to search by name, phone number, or email.
    • Make the search more flexible (e.g., partial matches for phone numbers or email addresses if that makes sense).
  3. Sort Contacts:

    • When view_contacts is called, display the contacts sorted alphabetically by name.
    • Hint: You’ll need to sort the contacts_list before iterating through it. The contacts_list.sort(key=lambda contact: contact.get('name', '').lower()) method or sorted_list = sorted(contacts_list, key=...) function can be used. A lambda function is helpful here to specify sorting by the ‘name’ value within each dictionary.
  4. More Robust Input Validation:

    • For phone numbers: Check if they only contain digits, and maybe if they have a certain length. (This can get complex due to international formats, so start simple).
    • For email addresses: A very basic check might be if it contains an “@” symbol and a “.”. (Proper email validation is very complex, so don’t aim for perfection here, just a basic check).
  5. Prevent Strictly Duplicate Contacts:

    • Before adding a new contact in add_contact, check if another contact with the exact same name AND exact same phone number already exists. If so, inform the user and don’t add the duplicate.
  6. Count Total Contacts:

    • Add a small feature, perhaps displayed with the menu or as a separate option, that shows the total number of contacts currently stored (i.e., len(contacts)).

These challenges will allow you to practice further and make your Contact Book even more feature-rich!