šØ Lesson 9.2: Building Your Project
You've got a plan. You've chosen your project, written requirements, designed your data model, and broken the work into phases. Now it's time to build. This lesson walks through the Contact Manager as a reference implementation, phase by phase. Follow along with the Contact Manager, or use the same phases to build your own chosen project. Either way, the approach is the same: build one phase at a time, test before moving on.
šÆ Learning Objectives
By the end of this lesson, you will have:
- Built a complete, working program using skills from the entire course
- Practiced incremental development ā building and testing one phase at a time
- Combined classes, functions, collections, file I/O, and error handling in one project
- Written code that persists data between sessions using JSON
- Experienced the satisfaction of seeing a plan turn into working software
Estimated Time: 90 minutes
Deliverable: A working capstone project
š In This Lesson
The Build Workflow
Follow this cycle for every phase: write the code, test it immediately, fix any bugs, then move on. Never move to the next phase until the current one works.
for one phase"] --> B["Run and
test it"] B --> C{"Works?"} C -->|"Yes"| D["Move to
next phase"] C -->|"No"| E["Debug and
fix"] E --> B style A fill:#3b82f6,color:#fff style B fill:#6366f1,color:#fff style D fill:#22c55e,color:#fff style E fill:#f59e0b,color:#fff
ā ļø The #1 Beginner Mistake
Writing the entire program before running it once. If you write 200 lines and then run it, you'll have 15 errors stacked on top of each other. Instead, write 10ā20 lines, run, verify, repeat. Small steps = small problems.
š Instructor Note: Delivery Guidance
This is a hands-on build session. In a classroom, the ideal format is: show the phase on screen, give students 10ā15 minutes to implement it in their chosen language, then quickly demo the solution. Walk the room during build time ā most students will get stuck on small issues (typos, missing imports, indentation). For self-paced students, the complete code at the end serves as a reference. Emphasize that the code in each phase is additive ā they should keep adding to the same file, not start a new one each phase. If students chose a different project (Weather Journal or Quiz Game), the phases still apply: data ā file I/O ā features ā UI ā polish.
Phase 1: Data Foundation
Start with the data class. This is the core of your project ā everything else depends on it.
š Phase 1 Checklist
- Create the Contact class with properties
- Add
to_dict()/from_dict()serialization methods - Add a
__str__/toString()method for display - Test: create objects, print them, convert to dict and back
import json
import os
FILENAME = "contacts.json"
class Contact:
"""Represents a single contact in the address book."""
def __init__(self, name, email="", phone="", category="General"):
self.name = name
self.email = email
self.phone = phone
self.category = category
def to_dict(self):
return {
"name": self.name,
"email": self.email,
"phone": self.phone,
"category": self.category,
}
@classmethod
def from_dict(cls, data):
return cls(
name=data["name"],
email=data.get("email", ""),
phone=data.get("phone", ""),
category=data.get("category", "General"),
)
def __str__(self):
return f"{self.name} | {self.email} | {self.phone} | {self.category}"
# ---- PHASE 1 TEST (delete after verifying) ----
if __name__ == "__main__":
c1 = Contact("Alice", "alice@example.com", "555-0101", "Friend")
c2 = Contact("Bob", "bob@work.com", "555-0202", "Work")
print(c1)
print(c2)
# Test serialization round-trip
d = c1.to_dict()
print(f"Dict: {d}")
c1_copy = Contact.from_dict(d)
print(f"Restored: {c1_copy}")
print("ā
Phase 1 passed!" if c1_copy.name == "Alice" else "ā Bug!")
const fs = require("fs");
const readline = require("readline");
const FILENAME = "contacts.json";
class Contact {
constructor(name, email = "", phone = "", category = "General") {
this.name = name;
this.email = email;
this.phone = phone;
this.category = category;
}
toJSON() {
return {
name: this.name,
email: this.email,
phone: this.phone,
category: this.category,
};
}
static fromJSON(data) {
return new Contact(
data.name,
data.email || "",
data.phone || "",
data.category || "General"
);
}
toString() {
return `${this.name} | ${this.email} | ${this.phone} | ${this.category}`;
}
}
// ---- PHASE 1 TEST (delete after verifying) ----
const c1 = new Contact("Alice", "alice@example.com", "555-0101", "Friend");
const c2 = new Contact("Bob", "bob@work.com", "555-0202", "Work");
console.log(c1.toString());
console.log(c2.toString());
const d = c1.toJSON();
console.log("Dict:", d);
const c1Copy = Contact.fromJSON(d);
console.log("Restored:", c1Copy.toString());
console.log(c1Copy.name === "Alice" ? "ā
Phase 1 passed!" : "ā Bug!");
using System.Text.Json;
using System.Text.Json.Serialization;
const string Filename = "contacts.json";
// ---- Contact class ----
class Contact
{
[JsonPropertyName("name")]
public string Name { get; set; } = "";
[JsonPropertyName("email")]
public string Email { get; set; } = "";
[JsonPropertyName("phone")]
public string Phone { get; set; } = "";
[JsonPropertyName("category")]
public string Category { get; set; } = "General";
public Contact() { }
public Contact(string name, string email = "", string phone = "", string category = "General")
{
Name = name; Email = email; Phone = phone; Category = category;
}
public override string ToString()
=> $"{Name} | {Email} | {Phone} | {Category}";
}
// ---- PHASE 1 TEST (delete after verifying) ----
var c1 = new Contact("Alice", "alice@example.com", "555-0101", "Friend");
var c2 = new Contact("Bob", "bob@work.com", "555-0202", "Work");
Console.WriteLine(c1);
Console.WriteLine(c2);
string json = JsonSerializer.Serialize(c1);
Console.WriteLine($"JSON: {json}");
var c1Copy = JsonSerializer.Deserialize<Contact>(json)!;
Console.WriteLine($"Restored: {c1Copy}");
Console.WriteLine(c1Copy.Name == "Alice" ? "ā
Phase 1 passed!" : "ā Bug!");
Run it now. You should see two contacts printed, a round-trip serialization test, and a "Phase 1 passed!" message. If you don't, fix it before moving on.
Phase 2: File Operations
Add save and load functions. These use the JSON skills from Lesson 23 and the file I/O from Lesson 22.
š Phase 2 Checklist
- Write
save_contacts()ā serialize list to JSON file - Write
load_contacts()ā load from JSON, handle missing file - Test: create contacts, save, load into new variable, verify match
def save_contacts(contacts, filename=FILENAME):
"""Save a list of Contact objects to a JSON file."""
data = [c.to_dict() for c in contacts]
with open(filename, "w") as f:
json.dump(data, f, indent=2)
print(f"š¾ Saved {len(contacts)} contacts to {filename}")
def load_contacts(filename=FILENAME):
"""Load contacts from a JSON file. Returns empty list if file missing."""
if not os.path.exists(filename):
return []
try:
with open(filename, "r") as f:
data = json.load(f)
contacts = [Contact.from_dict(d) for d in data]
print(f"š Loaded {len(contacts)} contacts from {filename}")
return contacts
except (json.JSONDecodeError, KeyError) as e:
print(f"ā ļø Error reading {filename}: {e}")
print("Starting with empty contact list.")
return []
# ---- PHASE 2 TEST ----
if __name__ == "__main__":
test_contacts = [
Contact("Alice", "alice@example.com", "555-0101", "Friend"),
Contact("Bob", "bob@work.com", "555-0202", "Work"),
]
save_contacts(test_contacts, "test_contacts.json")
loaded = load_contacts("test_contacts.json")
print(f"Loaded {len(loaded)} contacts")
for c in loaded:
print(f" {c}")
os.remove("test_contacts.json") # Clean up test file
print("ā
Phase 2 passed!" if len(loaded) == 2 else "ā Bug!")
function saveContacts(contacts, filename = FILENAME) {
const data = contacts.map(c => c.toJSON());
fs.writeFileSync(filename, JSON.stringify(data, null, 2));
console.log(`š¾ Saved ${contacts.length} contacts to ${filename}`);
}
function loadContacts(filename = FILENAME) {
if (!fs.existsSync(filename)) return [];
try {
const text = fs.readFileSync(filename, "utf-8");
const data = JSON.parse(text);
const contacts = data.map(d => Contact.fromJSON(d));
console.log(`š Loaded ${contacts.length} contacts from ${filename}`);
return contacts;
} catch (err) {
console.log(`ā ļø Error reading ${filename}: ${err.message}`);
console.log("Starting with empty contact list.");
return [];
}
}
// ---- PHASE 2 TEST ----
const testContacts = [
new Contact("Alice", "alice@example.com", "555-0101", "Friend"),
new Contact("Bob", "bob@work.com", "555-0202", "Work"),
];
saveContacts(testContacts, "test_contacts.json");
const loaded = loadContacts("test_contacts.json");
console.log(`Loaded ${loaded.length} contacts`);
loaded.forEach(c => console.log(` ${c}`));
fs.unlinkSync("test_contacts.json");
console.log(loaded.length === 2 ? "ā
Phase 2 passed!" : "ā Bug!");
static void SaveContacts(List<Contact> contacts, string filename = Filename)
{
var options = new JsonSerializerOptions { WriteIndented = true };
string json = JsonSerializer.Serialize(contacts, options);
File.WriteAllText(filename, json);
Console.WriteLine($"š¾ Saved {contacts.Count} contacts to {filename}");
}
static List<Contact> LoadContacts(string filename = Filename)
{
if (!File.Exists(filename)) return new List<Contact>();
try
{
string json = File.ReadAllText(filename);
var contacts = JsonSerializer.Deserialize<List<Contact>>(json) ?? new List<Contact>();
Console.WriteLine($"š Loaded {contacts.Count} contacts from {filename}");
return contacts;
}
catch (Exception ex)
{
Console.WriteLine($"ā ļø Error reading {filename}: {ex.Message}");
Console.WriteLine("Starting with empty contact list.");
return new List<Contact>();
}
}
// ---- PHASE 2 TEST ----
var testContacts = new List<Contact>
{
new Contact("Alice", "alice@example.com", "555-0101", "Friend"),
new Contact("Bob", "bob@work.com", "555-0202", "Work"),
};
SaveContacts(testContacts, "test_contacts.json");
var loadedList = LoadContacts("test_contacts.json");
Console.WriteLine($"Loaded {loadedList.Count} contacts");
foreach (var c in loadedList) Console.WriteLine($" {c}");
File.Delete("test_contacts.json");
Console.WriteLine(loadedList.Count == 2 ? "ā
Phase 2 passed!" : "ā Bug!");
Phase 3: Core Features
Now build the functions that do the actual work: add, display, search, edit, and delete contacts.
š Phase 3 Checklist
add_contact()ā prompt for info, create Contact, append to listdisplay_contacts()ā formatted table outputsearch_contacts()ā by name or categoryedit_contact()ā find by name, update fieldsdelete_contact()ā find by name, confirm, remove
def get_input(prompt, required=False):
"""Get user input with optional 'required' validation."""
while True:
value = input(prompt).strip()
if required and not value:
print("This field is required. Please try again.")
continue
return value
def add_contact(contacts):
"""Prompt user for contact info and add to the list."""
print("\n--- Add New Contact ---")
name = get_input("Name (required): ", required=True)
email = get_input("Email: ")
phone = get_input("Phone: ")
category = get_input("Category [General]: ") or "General"
contact = Contact(name, email, phone, category)
contacts.append(contact)
print(f"ā
Added: {contact}")
def display_contacts(contacts):
"""Display all contacts in a formatted table."""
if not contacts:
print("\nš No contacts yet.")
return
print(f"\n{'#':<4} {'Name':<20} {'Email':<25} {'Phone':<15} {'Category':<12}")
print("-" * 76)
for i, c in enumerate(contacts, 1):
print(f"{i:<4} {c.name:<20} {c.email:<25} {c.phone:<15} {c.category:<12}")
print(f"\nTotal: {len(contacts)} contacts")
def search_contacts(contacts):
"""Search contacts by name or category."""
if not contacts:
print("\nš No contacts to search.")
return
print("\nSearch by: (1) Name (2) Category")
choice = get_input("Choice: ")
term = get_input("Search term: ").lower()
if choice == "1":
results = [c for c in contacts if term in c.name.lower()]
elif choice == "2":
results = [c for c in contacts if term in c.category.lower()]
else:
print("Invalid choice.")
return
if not results:
print(f"No contacts found matching '{term}'.")
else:
print(f"\nFound {len(results)} result(s):")
for c in results:
print(f" ⢠{c}")
def edit_contact(contacts):
"""Find a contact by name and update their fields."""
if not contacts:
print("\nš No contacts to edit.")
return
name = get_input("Enter name of contact to edit: ").lower()
found = [c for c in contacts if name in c.name.lower()]
if not found:
print(f"No contact found matching '{name}'.")
return
if len(found) > 1:
print("Multiple matches:")
for i, c in enumerate(found, 1):
print(f" {i}. {c}")
idx = int(get_input("Which one? ")) - 1
contact = found[idx]
else:
contact = found[0]
print(f"Editing: {contact}")
print("(Press Enter to keep current value)")
contact.name = get_input(f"Name [{contact.name}]: ") or contact.name
contact.email = get_input(f"Email [{contact.email}]: ") or contact.email
contact.phone = get_input(f"Phone [{contact.phone}]: ") or contact.phone
contact.category = get_input(f"Category [{contact.category}]: ") or contact.category
print(f"ā
Updated: {contact}")
def delete_contact(contacts):
"""Find a contact by name and delete after confirmation."""
if not contacts:
print("\nš No contacts to delete.")
return
name = get_input("Enter name of contact to delete: ").lower()
found = [c for c in contacts if name in c.name.lower()]
if not found:
print(f"No contact found matching '{name}'.")
return
contact = found[0]
confirm = get_input(f"Delete '{contact.name}'? (y/n): ").lower()
if confirm == "y":
contacts.remove(contact)
print(f"šļø Deleted: {contact.name}")
else:
print("Cancelled.")
// Readline helper for async prompts
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const prompt = (q) => new Promise((resolve) => rl.question(q, resolve));
async function getInput(question, required = false) {
while (true) {
const value = (await prompt(question)).trim();
if (required && !value) {
console.log("This field is required. Please try again.");
continue;
}
return value;
}
}
async function addContact(contacts) {
console.log("\n--- Add New Contact ---");
const name = await getInput("Name (required): ", true);
const email = await getInput("Email: ");
const phone = await getInput("Phone: ");
const category = (await getInput("Category [General]: ")) || "General";
const contact = new Contact(name, email, phone, category);
contacts.push(contact);
console.log(`ā
Added: ${contact}`);
}
function displayContacts(contacts) {
if (contacts.length === 0) { console.log("\nš No contacts yet."); return; }
console.log(`\n${"#".padEnd(4)} ${"Name".padEnd(20)} ${"Email".padEnd(25)} ${"Phone".padEnd(15)} Category`);
console.log("-".repeat(76));
contacts.forEach((c, i) => {
console.log(`${String(i + 1).padEnd(4)} ${c.name.padEnd(20)} ${c.email.padEnd(25)} ${c.phone.padEnd(15)} ${c.category}`);
});
console.log(`\nTotal: ${contacts.length} contacts`);
}
async function searchContacts(contacts) {
if (contacts.length === 0) { console.log("\nš No contacts to search."); return; }
console.log("\nSearch by: (1) Name (2) Category");
const choice = await getInput("Choice: ");
const term = (await getInput("Search term: ")).toLowerCase();
let results;
if (choice === "1") results = contacts.filter(c => c.name.toLowerCase().includes(term));
else if (choice === "2") results = contacts.filter(c => c.category.toLowerCase().includes(term));
else { console.log("Invalid choice."); return; }
if (results.length === 0) console.log(`No contacts found matching '${term}'.`);
else {
console.log(`\nFound ${results.length} result(s):`);
results.forEach(c => console.log(` ⢠${c}`));
}
}
async function editContact(contacts) {
if (contacts.length === 0) { console.log("\nš No contacts to edit."); return; }
const name = (await getInput("Enter name of contact to edit: ")).toLowerCase();
const found = contacts.filter(c => c.name.toLowerCase().includes(name));
if (found.length === 0) { console.log(`No contact found matching '${name}'.`); return; }
const contact = found[0];
console.log(`Editing: ${contact}`);
console.log("(Press Enter to keep current value)");
contact.name = (await getInput(`Name [${contact.name}]: `)) || contact.name;
contact.email = (await getInput(`Email [${contact.email}]: `)) || contact.email;
contact.phone = (await getInput(`Phone [${contact.phone}]: `)) || contact.phone;
contact.category = (await getInput(`Category [${contact.category}]: `)) || contact.category;
console.log(`ā
Updated: ${contact}`);
}
async function deleteContact(contacts) {
if (contacts.length === 0) { console.log("\nš No contacts to delete."); return; }
const name = (await getInput("Enter name of contact to delete: ")).toLowerCase();
const idx = contacts.findIndex(c => c.name.toLowerCase().includes(name));
if (idx === -1) { console.log(`No contact found matching '${name}'.`); return; }
const confirm = (await getInput(`Delete '${contacts[idx].name}'? (y/n): `)).toLowerCase();
if (confirm === "y") {
console.log(`šļø Deleted: ${contacts[idx].name}`);
contacts.splice(idx, 1);
} else console.log("Cancelled.");
}
static string GetInput(string prompt, bool required = false)
{
while (true)
{
Console.Write(prompt);
string value = Console.ReadLine()?.Trim() ?? "";
if (required && string.IsNullOrEmpty(value))
{ Console.WriteLine("This field is required. Please try again."); continue; }
return value;
}
}
static void AddContact(List<Contact> contacts)
{
Console.WriteLine("\n--- Add New Contact ---");
string name = GetInput("Name (required): ", true);
string email = GetInput("Email: ");
string phone = GetInput("Phone: ");
string cat = GetInput("Category [General]: ");
if (string.IsNullOrEmpty(cat)) cat = "General";
var contact = new Contact(name, email, phone, cat);
contacts.Add(contact);
Console.WriteLine($"ā
Added: {contact}");
}
static void DisplayContacts(List<Contact> contacts)
{
if (contacts.Count == 0) { Console.WriteLine("\nš No contacts yet."); return; }
Console.WriteLine($"\n{"#",-4} {"Name",-20} {"Email",-25} {"Phone",-15} {"Category",-12}");
Console.WriteLine(new string('-', 76));
for (int i = 0; i < contacts.Count; i++)
{
var c = contacts[i];
Console.WriteLine($"{i + 1,-4} {c.Name,-20} {c.Email,-25} {c.Phone,-15} {c.Category,-12}");
}
Console.WriteLine($"\nTotal: {contacts.Count} contacts");
}
static void SearchContacts(List<Contact> contacts)
{
if (contacts.Count == 0) { Console.WriteLine("\nš No contacts to search."); return; }
Console.WriteLine("\nSearch by: (1) Name (2) Category");
string choice = GetInput("Choice: ");
string term = GetInput("Search term: ").ToLower();
List<Contact> results = choice switch
{
"1" => contacts.Where(c => c.Name.ToLower().Contains(term)).ToList(),
"2" => contacts.Where(c => c.Category.ToLower().Contains(term)).ToList(),
_ => new List<Contact>()
};
if (results.Count == 0) Console.WriteLine($"No contacts found matching '{term}'.");
else { Console.WriteLine($"\nFound {results.Count} result(s):"); results.ForEach(c => Console.WriteLine($" ⢠{c}")); }
}
static void EditContact(List<Contact> contacts)
{
if (contacts.Count == 0) { Console.WriteLine("\nš No contacts to edit."); return; }
string name = GetInput("Enter name of contact to edit: ").ToLower();
var contact = contacts.FirstOrDefault(c => c.Name.ToLower().Contains(name));
if (contact == null) { Console.WriteLine($"No contact found matching '{name}'."); return; }
Console.WriteLine($"Editing: {contact}");
Console.WriteLine("(Press Enter to keep current value)");
string val;
val = GetInput($"Name [{contact.Name}]: "); if (!string.IsNullOrEmpty(val)) contact.Name = val;
val = GetInput($"Email [{contact.Email}]: "); if (!string.IsNullOrEmpty(val)) contact.Email = val;
val = GetInput($"Phone [{contact.Phone}]: "); if (!string.IsNullOrEmpty(val)) contact.Phone = val;
val = GetInput($"Category [{contact.Category}]: "); if (!string.IsNullOrEmpty(val)) contact.Category = val;
Console.WriteLine($"ā
Updated: {contact}");
}
static void DeleteContact(List<Contact> contacts)
{
if (contacts.Count == 0) { Console.WriteLine("\nš No contacts to delete."); return; }
string name = GetInput("Enter name of contact to delete: ").ToLower();
var contact = contacts.FirstOrDefault(c => c.Name.ToLower().Contains(name));
if (contact == null) { Console.WriteLine($"No contact found matching '{name}'."); return; }
string confirm = GetInput($"Delete '{contact.Name}'? (y/n): ").ToLower();
if (confirm == "y") { contacts.Remove(contact); Console.WriteLine($"šļø Deleted: {contact.Name}"); }
else Console.WriteLine("Cancelled.");
}
Phase 4: Menu & Main Loop
Wire everything together with a menu-driven main loop. This is where your program becomes interactive.
def show_menu():
"""Display the main menu."""
print("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
print("ā š Contact Manager ā")
print("ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£")
print("ā 1. Add Contact ā")
print("ā 2. View All Contacts ā")
print("ā 3. Search Contacts ā")
print("ā 4. Edit Contact ā")
print("ā 5. Delete Contact ā")
print("ā 6. Save & Quit ā")
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
def main():
"""Main program loop."""
print("Welcome to Contact Manager!")
contacts = load_contacts()
while True:
show_menu()
choice = input("Enter choice (1-6): ").strip()
if choice == "1":
add_contact(contacts)
elif choice == "2":
display_contacts(contacts)
elif choice == "3":
search_contacts(contacts)
elif choice == "4":
edit_contact(contacts)
elif choice == "5":
delete_contact(contacts)
elif choice == "6":
save_contacts(contacts)
print("Goodbye! š")
break
else:
print("ā Invalid choice. Please enter 1-6.")
if __name__ == "__main__":
main()
function showMenu() {
console.log("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
console.log("ā š Contact Manager ā");
console.log("ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£");
console.log("ā 1. Add Contact ā");
console.log("ā 2. View All Contacts ā");
console.log("ā 3. Search Contacts ā");
console.log("ā 4. Edit Contact ā");
console.log("ā 5. Delete Contact ā");
console.log("ā 6. Save & Quit ā");
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
}
async function main() {
console.log("Welcome to Contact Manager!");
const contacts = loadContacts();
while (true) {
showMenu();
const choice = (await prompt("Enter choice (1-6): ")).trim();
if (choice === "1") await addContact(contacts);
else if (choice === "2") displayContacts(contacts);
else if (choice === "3") await searchContacts(contacts);
else if (choice === "4") await editContact(contacts);
else if (choice === "5") await deleteContact(contacts);
else if (choice === "6") {
saveContacts(contacts);
console.log("Goodbye! š");
rl.close();
break;
}
else console.log("ā Invalid choice. Please enter 1-6.");
}
}
main();
static void ShowMenu()
{
Console.WriteLine("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
Console.WriteLine("ā š Contact Manager ā");
Console.WriteLine("ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£");
Console.WriteLine("ā 1. Add Contact ā");
Console.WriteLine("ā 2. View All Contacts ā");
Console.WriteLine("ā 3. Search Contacts ā");
Console.WriteLine("ā 4. Edit Contact ā");
Console.WriteLine("ā 5. Delete Contact ā");
Console.WriteLine("ā 6. Save & Quit ā");
Console.WriteLine("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
}
// ---- Main program ----
Console.WriteLine("Welcome to Contact Manager!");
var contacts = LoadContacts();
while (true)
{
ShowMenu();
string choice = GetInput("Enter choice (1-6): ");
switch (choice)
{
case "1": AddContact(contacts); break;
case "2": DisplayContacts(contacts); break;
case "3": SearchContacts(contacts); break;
case "4": EditContact(contacts); break;
case "5": DeleteContact(contacts); break;
case "6":
SaveContacts(contacts);
Console.WriteLine("Goodbye! š");
return;
default:
Console.WriteLine("ā Invalid choice. Please enter 1-6.");
break;
}
}
ā Test It!
At this point your program should be fully functional. Test this sequence:
- Run the program ā it should show the menu
- Add 2-3 contacts
- View all contacts (formatted table)
- Search by name and by category
- Edit a contact
- Delete a contact
- Quit (saves to file)
- Run again ā your contacts should load from the file!
Phase 5: Polish & Error Handling
Your program works, but real users will find ways to break it. Phase 5 adds robustness.
š Phase 5 Checklist
- Handle empty contact list gracefully in all features
- Validate that required fields aren't empty
- Handle non-numeric input where numbers are expected
- Add a "press Enter to continue" pause after viewing results
- Catch KeyboardInterrupt / Ctrl+C gracefully
# Wrap main() to handle Ctrl+C gracefully:
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\nā ļø Interrupted. Saving contacts before exit...")
# Note: contacts isn't accessible here in this simple version.
# In a production app, you'd store contacts at module level
# or use a class-based approach.
print("Goodbye! š")
# Add to edit_contact() ā handle invalid index:
if len(found) > 1:
print("Multiple matches:")
for i, c in enumerate(found, 1):
print(f" {i}. {c}")
try:
idx = int(get_input("Which one? ")) - 1
if idx < 0 or idx >= len(found):
print("Invalid selection.")
return
contact = found[idx]
except ValueError:
print("Please enter a number.")
return
# Add a pause function for better UX:
def pause():
input("\nPress Enter to continue...")
// Handle process signals for graceful exit:
process.on("SIGINT", () => {
console.log("\n\nā ļø Interrupted. Goodbye! š");
rl.close();
process.exit(0);
});
// Add a pause function for better UX:
async function pause() {
await prompt("\nPress Enter to continue...");
}
// After display or search operations, add:
// await pause();
// Handle Ctrl+C gracefully:
Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true; // Prevent immediate termination
Console.WriteLine("\n\nā ļø Interrupted. Saving contacts before exit...");
SaveContacts(contacts);
Console.WriteLine("Goodbye! š");
Environment.Exit(0);
};
// Add a pause method:
static void Pause()
{
Console.Write("\nPress Enter to continue...");
Console.ReadLine();
}
The Complete Program
Here's the complete Contact Manager, all phases assembled. If you built a different project, your structure should be similar ā data class ā file I/O ā features ā menu ā error handling.
š View Complete Source Code (Python)
"""
Contact Manager ā Capstone Project
Programming Fundamentals: Three Language Approach
"""
import json
import os
FILENAME = "contacts.json"
# ==================== DATA MODEL ====================
class Contact:
def __init__(self, name, email="", phone="", category="General"):
self.name = name
self.email = email
self.phone = phone
self.category = category
def to_dict(self):
return {"name": self.name, "email": self.email,
"phone": self.phone, "category": self.category}
@classmethod
def from_dict(cls, data):
return cls(data["name"], data.get("email", ""),
data.get("phone", ""), data.get("category", "General"))
def __str__(self):
return f"{self.name} | {self.email} | {self.phone} | {self.category}"
# ==================== FILE OPERATIONS ====================
def save_contacts(contacts, filename=FILENAME):
data = [c.to_dict() for c in contacts]
with open(filename, "w") as f:
json.dump(data, f, indent=2)
print(f"š¾ Saved {len(contacts)} contacts to {filename}")
def load_contacts(filename=FILENAME):
if not os.path.exists(filename):
return []
try:
with open(filename, "r") as f:
data = json.load(f)
contacts = [Contact.from_dict(d) for d in data]
print(f"š Loaded {len(contacts)} contacts from {filename}")
return contacts
except (json.JSONDecodeError, KeyError) as e:
print(f"ā ļø Error reading {filename}: {e}")
return []
# ==================== HELPERS ====================
def get_input(prompt, required=False):
while True:
value = input(prompt).strip()
if required and not value:
print("This field is required. Please try again.")
continue
return value
# ==================== FEATURES ====================
def add_contact(contacts):
print("\n--- Add New Contact ---")
name = get_input("Name (required): ", required=True)
email = get_input("Email: ")
phone = get_input("Phone: ")
category = get_input("Category [General]: ") or "General"
contacts.append(Contact(name, email, phone, category))
print(f"ā
Added: {contacts[-1]}")
def display_contacts(contacts):
if not contacts:
print("\nš No contacts yet."); return
print(f"\n{'#':<4} {'Name':<20} {'Email':<25} {'Phone':<15} {'Category':<12}")
print("-" * 76)
for i, c in enumerate(contacts, 1):
print(f"{i:<4} {c.name:<20} {c.email:<25} {c.phone:<15} {c.category:<12}")
print(f"\nTotal: {len(contacts)} contacts")
def search_contacts(contacts):
if not contacts:
print("\nš No contacts to search."); return
print("\nSearch by: (1) Name (2) Category")
choice = get_input("Choice: ")
term = get_input("Search term: ").lower()
if choice == "1":
results = [c for c in contacts if term in c.name.lower()]
elif choice == "2":
results = [c for c in contacts if term in c.category.lower()]
else:
print("Invalid choice."); return
if not results:
print(f"No contacts found matching '{term}'.")
else:
print(f"\nFound {len(results)} result(s):")
for c in results: print(f" ⢠{c}")
def edit_contact(contacts):
if not contacts:
print("\nš No contacts to edit."); return
name = get_input("Enter name of contact to edit: ").lower()
found = [c for c in contacts if name in c.name.lower()]
if not found:
print(f"No contact found matching '{name}'."); return
contact = found[0]
print(f"Editing: {contact}\n(Press Enter to keep current value)")
contact.name = get_input(f"Name [{contact.name}]: ") or contact.name
contact.email = get_input(f"Email [{contact.email}]: ") or contact.email
contact.phone = get_input(f"Phone [{contact.phone}]: ") or contact.phone
contact.category = get_input(f"Category [{contact.category}]: ") or contact.category
print(f"ā
Updated: {contact}")
def delete_contact(contacts):
if not contacts:
print("\nš No contacts to delete."); return
name = get_input("Enter name of contact to delete: ").lower()
found = [c for c in contacts if name in c.name.lower()]
if not found:
print(f"No contact found matching '{name}'."); return
contact = found[0]
if get_input(f"Delete '{contact.name}'? (y/n): ").lower() == "y":
contacts.remove(contact)
print(f"šļø Deleted: {contact.name}")
else:
print("Cancelled.")
# ==================== MAIN ====================
def show_menu():
print("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
print("ā š Contact Manager ā")
print("ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā£")
print("ā 1. Add Contact ā")
print("ā 2. View All Contacts ā")
print("ā 3. Search Contacts ā")
print("ā 4. Edit Contact ā")
print("ā 5. Delete Contact ā")
print("ā 6. Save & Quit ā")
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
def main():
print("Welcome to Contact Manager!")
contacts = load_contacts()
while True:
show_menu()
choice = input("Enter choice (1-6): ").strip()
if choice == "1": add_contact(contacts)
elif choice == "2": display_contacts(contacts)
elif choice == "3": search_contacts(contacts)
elif choice == "4": edit_contact(contacts)
elif choice == "5": delete_contact(contacts)
elif choice == "6":
save_contacts(contacts)
print("Goodbye! š"); break
else: print("ā Invalid choice. Please enter 1-6.")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\nGoodbye! š")
š Instructor Note: Delivery Guidance
The complete program is provided as a reference, not something to copy. Students who built their own version will naturally differ ā that's good. In a classroom, have students demo their projects to each other (2 minutes each). Key things to watch for: Does it save/load correctly? Does search work? Is there a clean exit? Common issues: forgetting to call save_contacts() before exit, off-by-one errors in edit selection, and not handling empty lists. If students finished early, suggest they add one nice-to-have feature: CSV export, sorting, or category counts.
Testing Your Project
Before calling your project done, run through this test checklist:
ā Test Checklist
- Fresh start: Delete your data file. Does the program start with an empty list without crashing?
- Add contacts: Add 3+ contacts with different categories. Do they appear in the view?
- Search by name: Search for a partial name. Does it find the right contact(s)?
- Search by category: Search for a category. Does it filter correctly?
- Edit a contact: Change one field, keep others. Are the changes saved?
- Delete a contact: Delete one. Does the list update? Does "n" cancel the delete?
- Persistence: Quit the program, restart it. Are your contacts still there?
- Invalid input: Enter "99" at the menu. Does it show an error (not crash)?
- Empty operations: Try to search, edit, or delete with no contacts. Does it handle gracefully?
- Required fields: Try to add a contact with an empty name. Is it rejected?
š” The "Friend Test"
Hand your program to someone who didn't build it. Can they figure out how to use it without reading your code? If they get confused or it crashes, those are bugs to fix. Every crash is an error you forgot to handle.
Summary
š Congratulations ā You Built a Real Program!
If you've made it this far, you've just done something remarkable. You combined:
- Classes to model your data
- Functions to organize your logic
- Collections to store groups of objects
- File I/O and JSON to persist data between sessions
- Error handling to make your program robust
- Control flow for menus, loops, and user interaction
That's not a toy exercise ā that's the same architecture used in real applications. You should be proud of what you've built.
The Build Process
Data Class"] --> B["Phase 2
File I/O"] B --> C["Phase 3
Features"] C --> D["Phase 4
Main Loop"] D --> E["Phase 5
Polish"] E --> F["ā Done!"] style A fill:#3b82f6,color:#fff style B fill:#3b82f6,color:#fff style C fill:#6366f1,color:#fff style D fill:#6366f1,color:#fff style E fill:#22c55e,color:#fff style F fill:#f59e0b,color:#fff
š What's Next?
In Lesson 27: Code Review and Next Steps, you'll learn how to review your code with fresh eyes, refactor for clarity, and discover the paths forward for each language. You've built the foundation ā now let's make sure it's solid and figure out where to go from here.