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

  1. Instant lookup: "What's Eve's phone number?" is one line: contacts["Eve Santos"]["phone"]
  2. Self-documenting code: info["email"] is clear; row[3] is not
  3. Easy filtering: Find all math students with a dictionary comprehension
  4. Deduplication with sets: Send group texts without duplicates
  5. Extensibility: Adding a new field (like "parent_name") means adding one key to each contact dict — no restructuring needed

Discussion Questions

  1. 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.)

  2. 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?

  3. 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?

  4. 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.