Case Study: Building a Contact Book
This case study walks through building a practical contact management tool using dictionaries. The scenario is fictional but represents a common first project for students learning dictionaries.
The Scenario
Your friend Mika is starting a small tutoring business. They've been keeping track of their students' names and phone numbers on sticky notes, but it's getting out of hand. They have 30 students across three subjects, and they need to text parents regularly about schedule changes.
"Can you build me something? I just need to look up a name and get their phone number. Maybe organize by subject too."
This is a perfect dictionary problem. Let's build it.
Version 1: The Flat Contact Book
The simplest approach is a dictionary that maps names to phone numbers:
contacts = {
"Alice Park": "555-0101",
"Bob Chen": "555-0102",
"Carol Davis": "555-0103",
}
def lookup(contacts, name):
"""Look up a contact by name."""
if name in contacts:
print(f" {name}: {contacts[name]}")
else:
print(f" '{name}' not found.")
def add_contact(contacts, name, phone):
"""Add or update a contact."""
if name in contacts:
print(f" Updated {name}: {contacts[name]} -> {phone}")
else:
print(f" Added {name}: {phone}")
contacts[name] = phone
def remove_contact(contacts, name):
"""Remove a contact by name."""
if name in contacts:
removed = contacts.pop(name)
print(f" Removed {name} ({removed})")
else:
print(f" '{name}' not found — nothing to remove.")
# Test it
print("=== Lookup ===")
lookup(contacts, "Alice Park")
lookup(contacts, "Zara Khan")
print("\n=== Add ===")
add_contact(contacts, "Zara Khan", "555-0104")
add_contact(contacts, "Alice Park", "555-9999") # Update existing
print("\n=== Remove ===")
remove_contact(contacts, "Bob Chen")
remove_contact(contacts, "Nobody")
print(f"\n=== Final contacts ({len(contacts)}) ===")
for name, phone in sorted(contacts.items()):
print(f" {name}: {phone}")
Output:
=== Lookup ===
Alice Park: 555-0101
'Zara Khan' not found.
=== Add ===
Added Zara Khan: 555-0104
Updated Alice Park: 555-0101 -> 555-9999
=== Remove ===
Removed Bob Chen (555-0102)
'Nobody' not found — nothing to remove.
=== Final contacts (3) ===
Alice Park: 555-9999
Carol Davis: 555-0103
Zara Khan: 555-0104
This works, but it's limited. We can only store a phone number. What about email? Subject? Notes?
Version 2: Rich Contact Records
Let's upgrade each contact to a nested dictionary:
contacts = {
"Alice Park": {
"phone": "555-0101",
"email": "alice.park@email.com",
"subject": "math",
"notes": "Prefers Tuesday sessions",
},
"Bob Chen": {
"phone": "555-0102",
"email": "bob.chen@email.com",
"subject": "science",
"notes": "",
},
"Carol Davis": {
"phone": "555-0103",
"email": "carol.d@email.com",
"subject": "math",
"notes": "Parent: Diana Davis, 555-0110",
},
"Dave Kim": {
"phone": "555-0105",
"email": "dave.k@email.com",
"subject": "english",
"notes": "",
},
"Eve Santos": {
"phone": "555-0106",
"email": "eve.santos@email.com",
"subject": "math",
"notes": "Needs extra help with fractions",
},
}
def display_contact(name, info):
"""Display a single contact's details."""
print(f" Name: {name}")
print(f" Phone: {info['phone']}")
print(f" Email: {info['email']}")
print(f" Subject: {info['subject']}")
if info["notes"]:
print(f" Notes: {info['notes']}")
print()
def find_by_subject(contacts, subject):
"""Find all contacts enrolled in a given subject."""
matches = {
name: info
for name, info in contacts.items()
if info["subject"].lower() == subject.lower()
}
return matches
def search(contacts, query):
"""Search contacts by partial name match (case-insensitive)."""
query_lower = query.lower()
return {
name: info
for name, info in contacts.items()
if query_lower in name.lower()
}
# Display all math students
print("=== Math Students ===")
math_students = find_by_subject(contacts, "math")
for name, info in math_students.items():
display_contact(name, info)
# Search by partial name
print("=== Search: 'da' ===")
results = search(contacts, "da")
for name, info in results.items():
display_contact(name, info)
# Statistics
subjects = {}
for name, info in contacts.items():
subj = info["subject"]
subjects[subj] = subjects.get(subj, 0) + 1
print("=== Student Count by Subject ===")
for subj, count in sorted(subjects.items()):
print(f" {subj.capitalize()}: {count}")
Output:
=== Math Students ===
Name: Alice Park
Phone: 555-0101
Email: alice.park@email.com
Subject: math
Notes: Prefers Tuesday sessions
Name: Carol Davis
Phone: 555-0103
Email: carol.d@email.com
Subject: math
Notes: Parent: Diana Davis, 555-0110
Name: Eve Santos
Phone: 555-0106
Email: eve.santos@email.com
Subject: math
Notes: Needs extra help with fractions
=== Search: 'da' ===
Name: Carol Davis
Phone: 555-0103
Email: carol.d@email.com
Subject: math
Notes: Parent: Diana Davis, 555-0110
Name: Dave Kim
Phone: 555-0105
Email: dave.k@email.com
Subject: english
=== Student Count by Subject ===
English: 1
Math: 3
Science: 1
Version 3: Using Sets for Smart Operations
Mika wants to send a group text to all parents about a holiday schedule. But some students are enrolled in multiple subjects, and they don't want to text the same parent twice. Sets to the rescue:
math_students = {"Alice Park", "Carol Davis", "Eve Santos"}
science_students = {"Bob Chen", "Carol Davis", "Frank Lee"}
english_students = {"Dave Kim", "Eve Santos", "Frank Lee"}
# All unique students (no duplicates)
all_students = math_students | science_students | english_students
print(f"Total unique students: {len(all_students)}")
print(f" {sorted(all_students)}")
# Students in multiple subjects
multi_subject = (
(math_students & science_students) |
(math_students & english_students) |
(science_students & english_students)
)
print(f"\nStudents in 2+ subjects: {sorted(multi_subject)}")
# Students ONLY in math (not in science or english)
math_only = math_students - science_students - english_students
print(f"Math only: {sorted(math_only)}")
Output:
Total unique students: 6
['Alice Park', 'Bob Chen', 'Carol Davis', 'Dave Kim', 'Eve Santos', 'Frank Lee']
Students in 2+ subjects: ['Carol Davis', 'Eve Santos', 'Frank Lee']
Math only: ['Alice Park']
Analysis: Why Dictionaries?
Let's compare the dictionary approach to alternatives Mika might have used:
| Approach | Lookup Speed | Add/Remove | Extra Fields | Search by Subject |
|---|---|---|---|---|
| Sticky notes | O(n) — flip through all | Slow | Messy | Very slow |
| Spreadsheet | O(n) — scroll/search | Medium | Easy | Filter column |
| List of lists | O(n) — scan rows | append/remove |
Index by number (row[3]?) |
Loop + compare |
| Dictionary | O(1) — by name | Instant | Named fields | Comprehension |
The dictionary approach wins on almost every dimension. Lookup by name is O(1). Fields are named, not numbered. Filtering by subject is a clean one-liner with a comprehension.
What Mika Gained
- Instant lookup: "What's Eve's phone number?" is one line:
contacts["Eve Santos"]["phone"] - Self-documenting code:
info["email"]is clear;row[3]is not - Easy filtering: Find all math students with a dictionary comprehension
- Deduplication with sets: Send group texts without duplicates
- Extensibility: Adding a new field (like "parent_name") means adding one key to each contact dict — no restructuring needed
Discussion Questions
-
What happens if two students have the same name? How would you redesign the data structure to handle this? (Hint: consider using student IDs as keys instead of names.)
-
The search function does a linear scan through all contacts. For Mika's 30 students, this is fast enough. At what point would you need a more sophisticated search approach? What data structure might help?
-
In Version 3, we used three separate sets for each subject. How could you restructure the data so that subject membership is stored inside each contact's dictionary record, while still being able to do set operations?
-
This contact book lives only in memory — when the program exits, the data is gone. What would you need to add to make it persistent? (You'll learn how in Chapter 10.)
Mini-Project
Extend the contact book with these features:
- Bulk import: Write a function import_contacts(contacts, data_list) that takes a list of (name, phone, email, subject) tuples and adds them all to the contacts dictionary.
- Export: Write a function export_by_subject(contacts, subject) that returns a formatted string suitable for pasting into a text message (just names and phone numbers, one per line).
- Duplicate detection: Before adding a contact, check if a contact with the same phone number already exists under a different name.