Goal
You’ll build a complete, working program from scratch, directly applying classes, functions, error handling, and file operations from across this entire course.
Learn
Let’s build the contact book planned conceptually in the previous lesson, combining real choices from across this whole course:
class Contact:
def __init__(self, name, phone, email):
self.name = name
self.phone = phone
self.email = email
def __str__(self):
return f"{self.name}: {self.phone}, {self.email}"
contacts = []
def add_contact(name, phone, email):
contact = Contact(name, phone, email)
contacts.append(contact)
return contact
def find_contact(name):
for contact in contacts:
if contact.name.lower() == name.lower():
return contact
return None
def save_contacts(filename):
with open(filename, "w") as file:
for contact in contacts:
file.write(f"{contact.name},{contact.phone},{contact.email}\n")
add_contact("Ahmed", "555-0100", "ahmed@example.com")
add_contact("Sara", "555-0101", "sara@example.com")
found = find_contact("ahmed")
if found:
print(found)
else:
print("Contact not found.")
save_contacts("contacts.txt") Notice how directly this reflects earlier lessons: the Contact class with __init__ and __str__ (Parts 4.1 and 4.3), a plain list holding multiple objects (Part 2.3), separate focused functions for each distinct action (Part 3.1), case-insensitive comparison using .lower() (Part 1.3), and safe file writing using with (Part 4.4). None of this is new syntax — it’s the direct, combined application of everything already learned, exactly the point of a capstone Part.
Decision Task
In the example above, find_contact() returns None if no match is found, rather than raising an exception. Before reading on: why might returning None here be a more genuinely usable design than raising an error, given how the calling code actually uses the result?
Show Answer
The calling code specifically checks if found: to decide what to do next — a “not found” case is treated as a genuinely normal, expected possibility here, not an exceptional error condition. Returning None lets the calling code handle this cleanly with a simple conditional (Part 2.1), whereas raising an exception would require wrapping every call in try/except (Part 3.4) even for this completely ordinary, expected case, which would be more cumbersome than necessary for something this routine.
Common Mistake
Building a program like this correctly in isolation, but forgetting details covered in earlier, separate lessons simply because they feel disconnected from “building a real program” by the time you reach a capstone exercise — like using == instead of .lower() for the name comparison (missing Part 1.3’s case-sensitivity lesson), or manually calling open()/close() instead of with (missing Part 4.4’s file-safety lesson). Real programs need everything from every earlier lesson working together, not just the most recently covered material.
Practice Questions
1. Why does find_contact() use contact.name.lower() == name.lower() instead of just contact.name == name?
Show Answer
To make the comparison case-insensitive, following Part 1.3’s string methods lesson — so searching for “ahmed” correctly matches a contact stored as “Ahmed”, rather than requiring an exact case match.
2. What would be lost, specifically, if save_contacts() used open()/close() manually instead of with?
Show Answer
The guarantee that the file is properly closed even if an error occurs during writing (Part 4.4) — with a manual close(), an error partway through could leave the file improperly closed or with incomplete data.
3. Why does the Contact class define __str__ rather than relying on Python’s default object printing?
Show Answer
Following Part 4.3’s lesson — without __str__, printing a Contact would show an unhelpful memory address instead of the genuinely useful “name: phone, email” format defined here.
4. True or False: this example program would work identically well if contacts were tracked as plain dictionaries instead of a Contact class.
Show Answer
Partially true functionally, but using a class better matches Part 6.1’s planning reasoning — Contact genuinely has bundled data (name, phone, email) plus behavior (__str__), exactly the case where a class is the more natural, appropriate fit over a plain dictionary.
5. What earlier lesson’s reasoning explains why find_contact() returns None instead of raising an exception for a normal “not found” case?
Show Answer
Part 3.4’s error-handling lesson, and specifically the distinction between genuinely exceptional errors versus normal, expected program flow that doesn’t need exception handling at all.
Try It Yourself
Extend this example program by adding a remove_contact(name) function that removes a contact by name from the contacts list, using what you learned about lists in Part 2.3.
Show Answer
A reasonable answer: def remove_contact(name):\n contact = find_contact(name)\n if contact:\n contacts.remove(contact)\n return True\n return False — reusing find_contact() rather than duplicating the search logic, and using list.remove() from Part 2.3.
Quick Check
1. Why does find_contact() use .lower() on both sides of the comparison?
Show Answer
To make the name search case-insensitive, following Part 1.3’s string methods lesson.
2. Why does the Contact class define __str__?
Show Answer
So printing a Contact object shows a genuinely useful representation, rather than Python’s unhelpful default memory-address output.
3. Why does save_contacts() use with instead of manual open()/close()?
Show Answer
To guarantee the file is properly closed even if an error occurs during writing, following Part 4.4’s file-safety lesson.
4. Why does find_contact() return None instead of raising an exception when no match is found?
Show Answer
Because “not found” is a normal, expected case here, better handled with a simple conditional than exception handling reserved for genuinely exceptional situations.
5. What earlier-course concept does storing multiple Contact objects in one list directly apply?
Show Answer
Lists holding a genuine sequence of similar items, from Part 2.3.