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 useenumerate()
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 (likeFileNotFoundError
orjson.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:
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.contact_priya = { "name": "Priya Sharma", "phone": "9876543210", "email": "priya.sharma@example.com", "address": "123 Green Park, Delhi" }
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!
This structure allows us to have an ordered collection of our contacts, and each contact itself is a well-structured dictionary.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 ... ]
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 usepathlib.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.
- Purpose: Prompts the user to enter details for a new contact, creates a contact dictionary, and appends it to the
-
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.
- Purpose: Displays all contacts in the
-
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
.
- Purpose: Prompts the user for a search term (e.g., a name or part of a name). It then iterates through the
-
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:
- Prompt for a search term to find the contact to delete (similar to
search_contact
). - Display matching contacts with numbers (e.g., “1. Priya Sharma”, “2. Priya Kulkarni”).
- Ask the user to confirm which numbered contact they want to delete.
- Validate the user’s numerical input.
- Remove the chosen contact dictionary from the
contacts_list
(usingpop()
with the correct index).
- Prompt for a search term to find the contact to delete (similar to
- 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
(apathlib.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
forFileNotFoundError
andjson.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()
withindent=4
.
- Purpose: Saves the current
-
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. - Check
-
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 tofound_contacts
. - After the loop, if
found_contacts
is not empty, display them (perhaps usingview_contacts
logic or a simpler version). - If empty, print “No contacts found matching your search.”
- Return
found_contacts
(this can be useful fordelete_contact
).
- Get search term from user:
-
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 returnFalse
. - 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 usepop
if you have its index). Print success. ReturnTrue
. - 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. ReturnTrue
. - If user says no to confirmation or enters invalid number, return
False
.
- Get search term from user, similar to
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.
-
Overall Structure:
- Imports:
import json
is for working with JSON data (saving and loading).from pathlib import Path
gives us thePath
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 aPath
object. Using a constant makes it easy to change the filename later if needed, andPath
makes it OS-independent.- The main data is intended to be managed within the
run_contact_book_app
function in a list calledcontacts
. This list is populated byload_contacts_from_file
and passed to other functions.
- Imports:
-
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.
- This is a very simple function. Its sole responsibility is to
-
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 aPath
object (ourCONTACTS_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. Thewith
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 objectf
, 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
: Iffilepath.exists()
is false, or iffilepath.open()
were called on a non-existent file directly, this error would occur. The current structure handles it by returning[]
iffilepath.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. Theexcept
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).
-
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 Pythoncontacts_list
(which is a list of dictionaries) into JSON formatted text and writes it to the file objectf
.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 thecontacts_data.json
file human-readable if you were to open it in a text editor. Withoutindent
, 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.
- Purpose: This function is called whenever the list of contacts is modified (a contact is added or deleted) to save the current state of
-
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
, andaddress
usinginput()
..strip()
is used to remove any accidental leading/trailing whitespace from the user’s input. - Validation: It checks if
name
orphone
are empty (after stripping). If so, it prints an error and returnsFalse
(indicating the add operation failed). - Dictionary Creation: A new dictionary
new_contact
is created to store the details. Ifemail
oraddress
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 thecontacts_list
that was passed into the function.- It prints a success message and returns
True
.
- Purpose: To gather information for a new contact from the user and add it to the main
-
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
usingenumerate(contacts_list, start=1)
.enumerate
provides both aindex
(starting from 1 for user-friendly display) and thecontact
dictionary for each iteration. - Inside the loop, it prints the details of each
contact
dictionary in a formatted way. It usescontact.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 aKeyError
.
- Purpose: To display all the contacts stored in the
-
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 thecontacts_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).
-
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 tosearch_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 thatcontact_to_delete
is the actual dictionary object retrieved from thematches
list.list.remove()
finds the first occurrence of that exact object incontacts_list
and removes it. - It returns
True
on successful deletion,False
otherwise (e.g., if deletion is cancelled or input is invalid).
- Purpose: To remove a contact from the
-
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 thecontacts
list with data fromcontacts_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): Callsadd_contact(contacts)
. If it returnsTrue
(contact added), thensave_contacts_to_file(contacts, CONTACTS_FILEPATH)
is called immediately to make the new contact persistent.choice == '2'
(View): Callsview_contacts(contacts)
.choice == '3'
(Search): Callssearch_contact(contacts)
.choice == '4'
(Delete): Callsdelete_contact(contacts)
. If it returnsTrue
(contact deleted), thensave_contacts_to_file(contacts, CONTACTS_FILEPATH)
is called to persist the deletion.choice == '5'
(Exit): Prints a goodbye message, callssave_contacts_to_file
one last time (as a safeguard, though ideally data is already saved), and thenbreak
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.
- Initial Load:
-
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., viapython contact_book_app.py
). It preventsrun_contact_book_app()
from running if, for example, you were toimport
functions from this script into another Python file as a module.
- This standard Python construct ensures that
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
- Save: Save the code as
contact_book_app.py
in a directory likeexamples/contact_book/
. - Run: Open your terminal, navigate to that directory (
cd examples/contact_book
), and runpython contact_book_app.py
. - 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., inview_contacts
,search_contact
,delete_contact
). We usedenumerate
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()
withwith
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 (usingindent=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, andprint()
(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:
-
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.
-
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).
-
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. Thecontacts_list.sort(key=lambda contact: contact.get('name', '').lower())
method orsorted_list = sorted(contacts_list, key=...)
function can be used. Alambda
function is helpful here to specify sorting by the ‘name’ value within each dictionary.
- When
-
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).
-
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.
- Before adding a new contact in
-
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)
).
- 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.,
These challenges will allow you to practice further and make your Contact Book even more feature-rich!