Project: Contact Book
Store, find, and update contacts with a dictionary and predictable name matching.
Video lesson: Project: Contact Book
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
You have saved Ada Lovelace with an email address. Later you type her name in all capitals and accidentally leave spaces on both sides. Should the contact book say she does not exist? For a small teaching project, we want the search to find the same record. We also want adding Ada again with a new email address to update that record instead of making a duplicate. Those rules sound simple, but they affect how we choose dictionary keys and how we handle missing results. In this video we will run a broken lookup, inspect exactly why it fails, then build two small functions: one to add a contact and one to find one.
- 00:40
The example uses a dictionary in memory, so you can run everything in the site's browser console. It does not upload contacts or save them beyond this run. Before seeing any code, predict the final result: two distinct contacts, one updated Ada email, and a safe Not found outcome for a name that is absent. Open the companion project page to run each step and complete the checked task. The direct lookup fails with KeyError. The record exists, but not under the exact text we typed. Python dictionary keys are compared according to their values. The stored key begins with a capital A and has normal spacing; the searched key has two leading spaces, all-capital letters, and two trailing spaces.
- 01:26
They are different strings. We could try to remember perfect spelling every time, but that would make the search needlessly brittle. Another tempting fix is to use book.get on the raw search string. That would avoid the exception, but it would return None, so it still would not find Ada. We need a consistent rule for transforming names before both saving and searching. This video will use strip to remove surrounding whitespace and casefold to make the key insensitive to letter case. The broken run also teaches a useful debugging habit: print or inspect the exact key values. A name can look correct on screen while invisible spaces make it different.
- 02:08
Here is the rule in isolation. repr exposes the surrounding spaces in the searched text, which makes the original mismatch obvious. strip removes whitespace at the start and end. casefold gives us a case-insensitive form suitable for this example's keys. Both names become ada lovelace, so the final comparison is true. We must apply this transformation at the two places that matter: when assigning a contact and when looking one up. If only one side normalizes, the bug remains. We will still keep the readable display name inside each contact record. There is an important limitation: two different people can share the same name, and this rule would treat them as one contact.
- 02:53
A real address book needs a separate stable ID for each person and a policy for ambiguous searches. Our mini project is deliberately scoped to visible, testable create, update, and lookup behavior. The deeper website guide shows the collision and a simple ID-based alternative. The add function computes a normalized key and stores a small record with name and email fields. Nested dictionaries let a caller refer to record email by name, rather than remembering positions in a list. The find function applies the same normalization and uses get, which returns None if the key is absent.
- 03:33
Run the sample actions. Ada is added first, Grace second. The third action spells Ada differently and gives her a new email. Because the normalized key is the same, that assignment replaces her earlier record. It does not increase the contact count. The lookup also uses mixed case and spaces but finds the updated record. The printed count is two: Ada and Grace. We trim the email when saving because copied addresses often include accidental spaces. The program returns contact data from the lookup and prints only at the outer level, where a user interface can decide how to present it. This separation will make the project easier to extend later.
- 04:17
Notice that the stored record still has a name field even though the outer dictionary already has a key. The key is optimized for matching; the field is meant for showing a name to a person. If we printed the key everywhere, Ada would appear in lowercase, which may be technically consistent but looks careless in a contact list. The example updates the display name when we update the record. Another design might preserve the first display name or ask the user which spelling to keep. The important point is to choose the rule deliberately and test it. Let us test behaviors rather than just admire the two printed lines. The first assertion verifies that adding the same normalized name twice leaves only one record.
- 05:02
The second verifies both the update and the trimmed email. The third checks that an absent name returns None instead of crashing. When you use find_contact, inspect the result before indexing its email field; trying to index None would cause another error. On the website, the starter code leaves both functions unfinished. Make the prepared sample print the updated email and a count of two, then select Check task. The checker also tries a new name with extra spaces and checks the stored record, so a hard-coded Ada answer will not pass. Finish the short quiz to mark the project complete. As a next challenge, design how two different people named Alex could coexist.
- 05:46
You might use numeric IDs as primary keys and a separate search over names. Persistence is another extension: the browser example holds its data only during the run. This is enough to understand the logic before introducing files or a database. The full project page contains runnable examples, a deeper guide, common mistakes, and links to the relevant Python documentation.
Understand the concept
A contact book is more than a list of names. It needs a rule for deciding whether two entries refer to the same person, a way to retrieve a record, and a clear outcome when no record exists. We will use a dictionary whose keys are normalized names. Each value is another dictionary containing the display name and email address.
For lookup, strip extra whitespace from the name and apply casefold(). The strings Ada Lovelace and ada LOVELACE then share the same key. The display name can keep its original capitalization inside the record. Adding a contact with an existing normalized key updates that one record instead of creating a duplicate. This is a teaching example; real people can have the same name, so a real address book would use a separate unique identifier.
The browser example starts with an empty in-memory dictionary and a fixed list of actions. It does not save contacts to a server or to a file, and reloading the page will not make this sample a persistent address book. Keeping state visible makes the create, lookup, and update steps easier to verify before adding storage as a later extension.
- A normalized key makes lookup consistent
- Nested dictionaries keep named fields together
- Assigning an existing dictionary key updates its record
See it step by step
Read the code, predict the output, then compare it with the result.
01. Make a consistent lookup key
name = " Ada LOVELACE "
print(name.strip().casefold())ada lovelacestrip removes surrounding whitespace, and casefold makes a case-insensitive key for this example.
02. Store a named record
book = {}
book["ada lovelace"] = {"name": "Ada Lovelace", "email": "ada@example.com"}
print(book["ada lovelace"]["email"])
print(len(book))ada@example.com
1The outer dictionary looks up a person by key, while the inner dictionary names each contact field.
03. Update without adding a second entry
book = {"ada lovelace": {"name": "Ada Lovelace", "email": "old@example.com"}}
book["ada lovelace"] = {"name": "Ada Lovelace", "email": "new@example.com"}
print(book["ada lovelace"]["email"])
print(len(book))new@example.com
1Assigning the same normalized key replaces the record, so the contact count stays one.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Choose the key before the record
A dictionary needs a stable key for every contact. A raw typed name is fragile: extra spaces or a different letter case can turn one person into two entries. This teaching project uses name.strip().casefold() for both storage and lookup. The expression removes surrounding spaces and folds case, so Ada Lovelace and ADA LOVELACE share a key. Store the nicely capitalized name separately for display.
This rule is convenient but limited. Two different people can have the same name, and the example would overwrite the earlier one. That is why we explicitly describe this as a small learning project. A real address book should assign a separate stable identifier to each person and treat names as searchable fields. For now, the normalized key makes the state changes clear enough to inspect in the console.
typed = ["Ada Lovelace", " ADA LOVELACE ", "Grace Hopper"]
keys = [name.strip().casefold() for name in typed]
print(keys)
print(keys[0] == keys[1])['ada lovelace', 'ada lovelace', 'grace hopper']
True- strip removes only the surrounding whitespace in this example.
- casefold makes the two Ada spellings equal as lookup keys.
- Try an internal double space and observe that this simple rule does not merge it.
Add, update, and find records
Use one function to add a contact and another to find it. Both functions derive the key the same way. The value is a small dictionary with explicit name and email fields, so callers do not have to remember that index zero means name and index one means email. Trim the displayed name and email when saving them; keep the stored display name readable instead of replacing it with the folded key.
Assigning book[key] creates a contact when the key is new and updates one when it already exists. The sample adds Ada, then updates her email with different capitalization and surrounding spaces. The dictionary length stays one. For lookup, book.get(key) returns the record or None. That allows the caller to decide whether to print the email or a Not found message without raising KeyError.
def add_contact(book, name, email):
key = name.strip().casefold()
book[key] = {"name": name.strip(), "email": email.strip()}
def find_contact(book, name):
return book.get(name.strip().casefold())
book = {}
add_contact(book, "Ada Lovelace", "ada@example.com")
add_contact(book, " ada LOVELACE ", " ada.new@example.com ")
print(find_contact(book, "ADA LOVELACE")["email"])
print(len(book))ada.new@example.com
1- The second add computes the same normalized key and replaces Ada's record.
- The lookup normalizes its input and retrieves the updated email.
- Change the last lookup to Grace Hopper and handle its None result before indexing.
Handle a missing name and a real-world collision
A missing lookup is normal in a search feature, so decide how to express it. Returning None keeps the function simple. The caller checks the result before reading record['email']; indexing None would raise TypeError. The example prints Not found for a missing person, then shows a safer storage shape for a larger program: numeric IDs as dictionary keys. Two people named Alex can then coexist.
IDs alone do not implement search or storage, but they reveal why display names should not be treated as unique. A larger contact book would probably keep records by ID, build a separate index for name searches, define what to do with ambiguous matches, and persist data between sessions. The browser task intentionally stops at create, update, and lookup so each behavior can be checked with a small deterministic test.
book = {"ada lovelace": {"name": "Ada Lovelace", "email": "ada@example.com"}}
missing = book.get("grace hopper")
print(missing["email"] if missing else "Not found")
by_id = {
101: {"name": "Alex", "email": "alex.one@example.com"},
102: {"name": "Alex", "email": "alex.two@example.com"},
}
print(len(by_id))
print(by_id[102]["email"])Not found
2
alex.two@example.com- The missing key returns None, so the conditional avoids indexing it.
- Separate integer IDs allow two Alex records to exist without overwriting one another.
- Before changing the code, predict what indexing by_id[103] would do.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
In add_contact, set key = name.strip().casefold(); assign book[key] = {'name': name.strip(), 'email': email.strip()}. In find_contact, return book.get(name.strip().casefold()).
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Normalizing names only when saving
book[name.strip().casefold()] = record
found = book.get(search_name)book[name.strip().casefold()] = record
found = book.get(search_name.strip().casefold())What happens: Searching for ADA LOVELACE misses an existing contact.
Apply the same rule to both the saved and searched name.
Indexing a missing contact directly
record = book["missing person"]record = book.get("missing person")What happens: The program raises KeyError instead of reporting a missing contact.
get returns None for an absent key, allowing the caller to show a friendly result.
Assuming names identify real people uniquely
book[person_name] = email # two people can share this name# For real data, use a separate stable contact ID as the key.What happens: Adding a second person with the same name overwrites the first.
Normalized names are convenient for this exercise but cannot serve as reliable unique identifiers in a real address book.