🗂️ Lesson 5.2: Dictionaries, Objects, and Maps
Lists store values by position. But what if you want to look something up by name? That's where key-value collections come in — store a value under a meaningful key, then retrieve it instantly without searching.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Create dictionaries, objects, and maps in all three languages
- Access, add, update, and delete key-value pairs
- Handle missing keys safely
- Iterate over keys, values, and key-value pairs
- Understand JavaScript objects vs. Maps
- Nest dictionaries for complex data structures
Estimated Time: 45 minutes
Project: Build a contact book with add, search, update, and delete
📑 In This Lesson
Creating Key-Value Collections
A key-value collection stores data as pairs: each key is a unique label, and each value is the data associated with it. Think of it like a real dictionary — you look up a word (key) to find its definition (value).
# Python uses "dictionaries" (dict) — curly braces with key: value
person = {
"name": "Alice",
"age": 30,
"city": "New York",
"is_student": False
}
# Empty dictionary
empty = {}
# or: empty = dict()
# From a list of pairs
pairs = dict([("a", 1), ("b", 2), ("c", 3)])
# From keyword arguments
settings = dict(theme="dark", font_size=16, language="en")
print(person)
print(type(person)) # <class 'dict'>
print(len(person)) # 4 (number of key-value pairs)
// JavaScript uses "objects" — curly braces with key: value
let person = {
name: "Alice", // Keys don't need quotes (if valid identifiers)
age: 30,
city: "New York",
isStudent: false
};
// Empty object
let empty = {};
// Keys with special characters need quotes:
let config = {
"font-size": 16, // Hyphens need quotes
"max retries": 3 // Spaces need quotes
};
console.log(person);
console.log(typeof person); // 'object'
console.log(Object.keys(person).length); // 4
// C# uses Dictionary<TKey, TValue> — strongly typed
Dictionary<string, object> person = new Dictionary<string, object>
{
{ "name", "Alice" },
{ "age", 30 },
{ "city", "New York" },
{ "isStudent", false }
};
// More commonly, use a specific value type:
Dictionary<string, string> capitals = new Dictionary<string, string>
{
{ "France", "Paris" },
{ "Japan", "Tokyo" },
{ "Brazil", "Brasilia" }
};
// Empty dictionary
Dictionary<string, int> empty = new Dictionary<string, int>();
Console.WriteLine(capitals.Count); // 3
💡 Lists vs. Dictionaries
Lists/arrays are like numbered lockers — you access item #0, #1, #2. Dictionaries are like labeled mailboxes — you access the one labeled "name" or "age." Use lists when order and position matter; use dictionaries when you want to look things up by a meaningful key.
🎓 Instructor Note: Delivery Guidance
The mailbox analogy resonates well. Draw a visual: a row of labeled slots ("name" → "Alice", "age" → 30). Emphasize that keys must be unique — you can't have two "name" entries. Also note that Python dicts and JS objects use nearly identical syntax ({ } with key-value pairs), which is a nice symmetry for students learning both. C#'s syntax is more verbose but the concept is identical.
Accessing Values
Use the key to retrieve its associated value — like looking up a word in a dictionary.
person = {"name": "Alice", "age": 30, "city": "New York"}
# Bracket notation
print(person["name"]) # "Alice"
print(person["age"]) # 30
# ❌ Missing key = KeyError
# print(person["email"]) # KeyError: 'email'
# Safe access with .get() — returns None (or a default) if missing
print(person.get("email")) # None
print(person.get("email", "N/A")) # "N/A" (custom default)
print(person.get("name", "N/A")) # "Alice" (key exists, returns value)
# .get() is the preferred way to access when key might not exist
let person = { name: "Alice", age: 30, city: "New York" };
// Dot notation (most common)
console.log(person.name); // "Alice"
console.log(person.age); // 30
// Bracket notation (required for special keys or variables)
console.log(person["city"]); // "New York"
let key = "name";
console.log(person[key]); // "Alice" (dynamic key lookup)
// Missing key = undefined (not an error!)
console.log(person.email); // undefined
console.log(person.email ?? "N/A"); // "N/A" (nullish coalescing)
// Optional chaining for nested access (safe)
console.log(person.address?.street); // undefined (no error)
Dictionary<string, string> person = new Dictionary<string, string>
{
{ "name", "Alice" },
{ "age", "30" },
{ "city", "New York" }
};
// Bracket notation
Console.WriteLine(person["name"]); // "Alice"
Console.WriteLine(person["age"]); // "30"
// ❌ Missing key = KeyNotFoundException
// Console.WriteLine(person["email"]);
// Safe access with TryGetValue
if (person.TryGetValue("email", out string? email))
{
Console.WriteLine(email);
}
else
{
Console.WriteLine("N/A"); // "N/A"
}
// Or use GetValueOrDefault (with newer C#):
string city = person.GetValueOrDefault("city", "Unknown");
Console.WriteLine(city); // "New York"
Access Patterns Quick Reference
| Pattern | 🐍 Python | ⚡ JavaScript | 🔷 C# |
|---|---|---|---|
| Direct access | d["key"] |
obj.key or obj["key"] |
d["key"] |
| Safe access | d.get("key", default) |
obj.key ?? default |
TryGetValue() |
| Missing key | KeyError |
undefined |
KeyNotFoundException |
⚠️ JavaScript's Quiet undefined
JavaScript returns undefined for missing keys instead of throwing an error. This can hide bugs — your code keeps running with undefined values instead of failing loudly. Always use ?? (nullish coalescing) or check explicitly when a key might be absent.
Adding and Updating
Adding a new key and updating an existing key use the same syntax — just assign to the key.
person = {"name": "Alice", "age": 30}
# Add a new key
person["email"] = "alice@example.com"
print(person)
# {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}
# Update an existing key
person["age"] = 31
print(person["age"]) # 31
# Add/update multiple keys at once
person.update({
"city": "Boston",
"age": 32, # Updates existing
"job": "developer" # Adds new
})
print(person)
# Merge two dicts (Python 3.9+)
defaults = {"theme": "light", "language": "en"}
overrides = {"theme": "dark", "font_size": 16}
merged = defaults | overrides
print(merged) # {'theme': 'dark', 'language': 'en', 'font_size': 16}
let person = { name: "Alice", age: 30 };
// Add a new key
person.email = "alice@example.com";
// or: person["email"] = "alice@example.com";
console.log(person);
// Update an existing key
person.age = 31;
console.log(person.age); // 31
// Add/update multiple keys (spread operator)
person = {
...person,
city: "Boston",
age: 32,
job: "developer"
};
// Merge two objects
let defaults = { theme: "light", language: "en" };
let overrides = { theme: "dark", fontSize: 16 };
let merged = { ...defaults, ...overrides };
console.log(merged); // {theme: 'dark', language: 'en', fontSize: 16}
// Object.assign also works:
// let merged = Object.assign({}, defaults, overrides);
Dictionary<string, string> person = new Dictionary<string, string>
{
{ "name", "Alice" },
{ "age", "30" }
};
// Add a new key
person["email"] = "alice@example.com";
// or: person.Add("email", "alice@example.com");
// ⚠️ .Add() throws if key already exists; [] overwrites silently
// Update an existing key
person["age"] = "31";
// Add/update multiple — no built-in merge, use a loop:
var updates = new Dictionary<string, string>
{
{ "city", "Boston" },
{ "age", "32" },
{ "job", "developer" }
};
foreach (var kvp in updates)
{
person[kvp.Key] = kvp.Value; // Adds or updates
}
Console.WriteLine(string.Join(", ",
person.Select(kvp => $"{kvp.Key}: {kvp.Value}")));
💡 Same Syntax, Two Operations
In all three languages, collection["key"] = value does double duty: if the key exists, it updates the value; if it doesn't, it creates a new entry. This makes dictionaries very convenient — you don't need separate "add" and "update" operations.
Removing Entries
person = {"name": "Alice", "age": 30, "city": "NYC", "email": "a@b.com"}
# Remove by key and get the value
removed = person.pop("email")
print(removed) # "a@b.com"
print(person) # No more 'email' key
# Safe removal (returns default if key missing)
missing = person.pop("phone", "not found")
print(missing) # "not found" (no error)
# Remove by key (no return value)
del person["city"]
print(person) # {'name': 'Alice', 'age': 30}
# Remove and get the last inserted pair
last_key, last_val = person.popitem()
print(f"Removed: {last_key} = {last_val}")
# Clear everything
person.clear()
print(person) # {}
let person = { name: "Alice", age: 30, city: "NYC", email: "a@b.com" };
// Remove by key
delete person.email;
console.log(person); // No more 'email'
// delete returns true/false (but doesn't give you the value)
let existed = delete person.city;
console.log(existed); // true
// To get the value before deleting:
let { age, ...rest } = person; // Destructure: age gets the value, rest is the remainder
console.log(age); // 30
console.log(rest); // { name: "Alice" }
// Clear everything (reassign)
person = {};
// or: Object.keys(person).forEach(k => delete person[k]);
Dictionary<string, string> person = new Dictionary<string, string>
{
{ "name", "Alice" },
{ "age", "30" },
{ "city", "NYC" },
{ "email", "a@b.com" }
};
// Remove by key (returns true if found, false if not)
bool removed = person.Remove("email");
Console.WriteLine(removed); // True
// Remove and get the value
if (person.Remove("city", out string? cityValue))
{
Console.WriteLine($"Removed city: {cityValue}"); // "NYC"
}
// Remove missing key — no error, just returns false
bool notFound = person.Remove("phone");
Console.WriteLine(notFound); // False
// Clear everything
person.Clear();
Console.WriteLine(person.Count); // 0
Checking for Keys
Before accessing a key, you often want to check if it exists first.
person = {"name": "Alice", "age": 30, "city": "NYC"}
# Check if key exists
print("name" in person) # True
print("email" in person) # False
print("email" not in person) # True
# Common pattern: check then access
if "email" in person:
print(person["email"])
else:
print("No email on file")
# Or just use .get() (usually simpler)
email = person.get("email", "No email on file")
print(email)
# Get all keys, values, or pairs
print(list(person.keys())) # ['name', 'age', 'city']
print(list(person.values())) # ['Alice', 30, 'NYC']
print(list(person.items())) # [('name','Alice'), ('age',30), ('city','NYC')]
let person = { name: "Alice", age: 30, city: "NYC" };
// Check if key exists
console.log("name" in person); // true
console.log("email" in person); // false
console.log(person.hasOwnProperty("name")); // true
// Check then access
if ("email" in person) {
console.log(person.email);
} else {
console.log("No email on file");
}
// Or use nullish coalescing
let email = person.email ?? "No email on file";
// Get all keys, values, or pairs
console.log(Object.keys(person)); // ['name', 'age', 'city']
console.log(Object.values(person)); // ['Alice', 30, 'NYC']
console.log(Object.entries(person)); // [['name','Alice'], ['age',30], ...]
Dictionary<string, string> person = new Dictionary<string, string>
{
{ "name", "Alice" },
{ "age", "30" },
{ "city", "NYC" }
};
// Check if key exists
Console.WriteLine(person.ContainsKey("name")); // True
Console.WriteLine(person.ContainsKey("email")); // False
// Check if value exists
Console.WriteLine(person.ContainsValue("Alice")); // True
// Check then access
if (person.ContainsKey("email"))
Console.WriteLine(person["email"]);
else
Console.WriteLine("No email on file");
// Or use TryGetValue (more efficient — single lookup)
if (person.TryGetValue("email", out string? val))
Console.WriteLine(val);
else
Console.WriteLine("No email on file");
// Get all keys, values, or pairs
Console.WriteLine(string.Join(", ", person.Keys)); // name, age, city
Console.WriteLine(string.Join(", ", person.Values)); // Alice, 30, NYC
Iterating Over Collections
Looping through key-value pairs is something you'll do constantly — displaying settings, processing records, transforming data.
scores = {"Alice": 95, "Bob": 87, "Charlie": 92}
# Iterate over keys (default)
for name in scores:
print(name) # Alice, Bob, Charlie
# Iterate over values
for score in scores.values():
print(score) # 95, 87, 92
# Iterate over key-value pairs (most common)
for name, score in scores.items():
print(f"{name}: {score}")
# Alice: 95
# Bob: 87
# Charlie: 92
# Dict comprehension (like list comprehension!)
doubled = {name: score * 2 for name, score in scores.items()}
print(doubled) # {'Alice': 190, 'Bob': 174, 'Charlie': 184}
# Filter with dict comprehension
honor_roll = {name: score for name, score in scores.items() if score >= 90}
print(honor_roll) # {'Alice': 95, 'Charlie': 92}
let scores = { Alice: 95, Bob: 87, Charlie: 92 };
// for...in loop (iterates over keys)
for (let name in scores) {
console.log(`${name}: ${scores[name]}`);
}
// Object.entries() with for...of (cleaner)
for (let [name, score] of Object.entries(scores)) {
console.log(`${name}: ${score}`);
}
// Object.keys() and Object.values()
Object.keys(scores).forEach(name => console.log(name));
Object.values(scores).forEach(score => console.log(score));
// Transform into a new object (no built-in comprehension)
let doubled = Object.fromEntries(
Object.entries(scores).map(([name, score]) => [name, score * 2])
);
console.log(doubled); // {Alice: 190, Bob: 174, Charlie: 184}
// Filter
let honorRoll = Object.fromEntries(
Object.entries(scores).filter(([_, score]) => score >= 90)
);
console.log(honorRoll); // {Alice: 95, Charlie: 92}
Dictionary<string, int> scores = new Dictionary<string, int>
{
{ "Alice", 95 },
{ "Bob", 87 },
{ "Charlie", 92 }
};
// foreach over key-value pairs
foreach (KeyValuePair<string, int> kvp in scores)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
// Shorthand with var
foreach (var (name, score) in scores)
{
Console.WriteLine($"{name}: {score}");
}
// Iterate keys or values
foreach (string name in scores.Keys)
Console.WriteLine(name);
foreach (int score in scores.Values)
Console.WriteLine(score);
// Transform with LINQ
var doubled = scores.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value * 2
);
// Filter with LINQ
var honorRoll = scores
.Where(kvp => kvp.Value >= 90)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
Console.WriteLine(string.Join(", ",
honorRoll.Select(kvp => $"{kvp.Key}: {kvp.Value}")));
🎓 Instructor Note: Delivery Guidance
The for name, score in scores.items() pattern (Python) and for (let [name, score] of Object.entries(scores)) (JS) are the patterns students will use most. Emphasize destructuring — pulling both key and value out in one step. Dict/object comprehensions are a "level up" feature; show them but don't expect beginners to master them immediately. C#'s KeyValuePair is verbose, so show the var (name, score) shorthand.
JavaScript: Objects vs. Maps
JavaScript has two key-value types. Plain objects are the default. Maps are a newer, more powerful alternative for certain use cases.
# Python only has dict — it handles everything
# No need to choose between two types
scores = {}
scores["Alice"] = 95
scores[42] = "numeric key" # Any hashable type as key
scores[(1, 2)] = "tuple key" # Even tuples work!
print(len(scores)) # 3
# Python dicts are already ordered (insertion order, since 3.7)
# Python dicts already support any hashable key
# Python dicts already have a clean iteration API
# So Python's dict = best of both JS objects and Maps
// OBJECTS: Great for structured data (like a person or config)
let person = { name: "Alice", age: 30 };
person.email = "alice@example.com";
// MAP: Great for dynamic key-value storage
let scores = new Map();
scores.set("Alice", 95);
scores.set("Bob", 87);
scores.set(42, "numeric key!"); // Non-string keys work!
scores.set(true, "boolean key!"); // Any type as key!
console.log(scores.get("Alice")); // 95
console.log(scores.size); // 4
console.log(scores.has("Bob")); // true
scores.delete("Bob");
console.log(scores.size); // 3
// Iterating a Map
for (let [key, value] of scores) {
console.log(`${key} => ${value}`);
}
// WHEN TO USE WHICH:
// Object: structured data with known string keys (person, config, API response)
// Map: dynamic lookups, non-string keys, frequent add/delete, need .size
// C# Dictionary<TKey, TValue> handles both use cases
// Keys can be any type (like JS Maps)
Dictionary<string, int> stringKeys = new Dictionary<string, int>
{
{ "Alice", 95 },
{ "Bob", 87 }
};
Dictionary<int, string> intKeys = new Dictionary<int, string>
{
{ 1, "first" },
{ 42, "answer" }
};
// C# also has SortedDictionary (keeps keys sorted)
SortedDictionary<string, int> sorted = new SortedDictionary<string, int>
{
{ "Charlie", 92 },
{ "Alice", 95 },
{ "Bob", 87 }
};
foreach (var kvp in sorted)
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
// Alice: 95, Bob: 87, Charlie: 92 (alphabetical!)
💡 Object vs. Map Quick Decision
In JavaScript, use objects when your keys are strings and you're modeling a "thing" (person, config, API response). Use Maps when you need non-string keys, when keys are dynamic/unknown, when you frequently add and delete entries, or when you need to know the .size. When in doubt, start with an object.
Nested Structures
Values can be anything — including other dictionaries or lists. This lets you model complex, real-world data.
# A student record with nested data
student = {
"name": "Alice",
"grades": {
"math": 95,
"english": 88,
"science": 92
},
"hobbies": ["reading", "chess", "hiking"],
"address": {
"street": "123 Main St",
"city": "Boston",
"state": "MA"
}
}
# Access nested values by chaining keys
print(student["grades"]["math"]) # 95
print(student["hobbies"][0]) # "reading"
print(student["address"]["city"]) # "Boston"
# Modify nested values
student["grades"]["english"] = 91
student["hobbies"].append("coding")
# Safe nested access
phone = student.get("contact", {}).get("phone", "N/A")
print(phone) # "N/A"
let student = {
name: "Alice",
grades: {
math: 95,
english: 88,
science: 92
},
hobbies: ["reading", "chess", "hiking"],
address: {
street: "123 Main St",
city: "Boston",
state: "MA"
}
};
// Access nested values
console.log(student.grades.math); // 95
console.log(student.hobbies[0]); // "reading"
console.log(student.address.city); // "Boston"
// Modify nested values
student.grades.english = 91;
student.hobbies.push("coding");
// Safe nested access with optional chaining
let phone = student.contact?.phone ?? "N/A";
console.log(phone); // "N/A"
// Destructure nested objects
let { name, grades: { math }, address: { city } } = student;
console.log(`${name} got ${math} in math, lives in ${city}`);
// C# typically models complex data with classes (Module 6),
// but nested dictionaries work too:
var student = new Dictionary<string, object>
{
{ "name", "Alice" },
{ "grades", new Dictionary<string, int>
{
{ "math", 95 },
{ "english", 88 },
{ "science", 92 }
}
},
{ "hobbies", new List<string> { "reading", "chess", "hiking" } },
{ "address", new Dictionary<string, string>
{
{ "street", "123 Main St" },
{ "city", "Boston" },
{ "state", "MA" }
}
}
};
// Access nested values (requires casting)
var grades = (Dictionary<string, int>)student["grades"];
Console.WriteLine(grades["math"]); // 95
var hobbies = (List<string>)student["hobbies"];
Console.WriteLine(hobbies[0]); // "reading"
// This casting is clunky — it's why C# prefers classes for nested data.
// We'll learn a much cleaner approach in Module 6!
💡 This Is How Real Data Looks
API responses, configuration files, database records, game save files — they're almost always nested structures like this. When you work with JSON data (Lesson 23), you'll see this exact pattern everywhere. Getting comfortable with nested access now will pay off enormously.
Exercises
🏋️ Exercise 1: Word Frequency Counter
Objective: Given a sentence, count how many times each word appears. Store the results in a dictionary/object. Make it case-insensitive.
Input: "the cat sat on the mat the cat"
Expected output: {"the": 3, "cat": 2, "sat": 1, "on": 1, "mat": 1}
✅ Solution
sentence = "the cat sat on the mat the cat"
words = sentence.lower().split()
# Method 1: Manual counting
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)
# Method 2: collections.Counter (built-in!)
from collections import Counter
counts2 = Counter(words)
print(dict(counts2))
let sentence = "the cat sat on the mat the cat";
let words = sentence.toLowerCase().split(" ");
let counts = {};
for (let word of words) {
counts[word] = (counts[word] ?? 0) + 1;
}
console.log(counts);
// Or with reduce:
let counts2 = words.reduce((acc, word) => {
acc[word] = (acc[word] ?? 0) + 1;
return acc;
}, {});
console.log(counts2);
string sentence = "the cat sat on the mat the cat";
string[] words = sentence.ToLower().Split(' ');
Dictionary<string, int> counts = new Dictionary<string, int>();
foreach (string word in words)
{
if (counts.ContainsKey(word))
counts[word]++;
else
counts[word] = 1;
}
foreach (var kvp in counts)
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
// Or with LINQ GroupBy:
var counts2 = words.GroupBy(w => w)
.ToDictionary(g => g.Key, g => g.Count());
🏋️ Exercise 2: Inventory Tracker
Objective: Create functions to manage a store inventory:
add_item(inventory, item, quantity)— add quantity (or increase if exists)sell_item(inventory, item, quantity)— reduce quantity (can't go below 0, error if not in stock)check_stock(inventory, item)— return the quantity (0 if not found)low_stock(inventory, threshold)— return all items at or below the threshold
✅ Solution
def add_item(inventory, item, quantity):
inventory[item] = inventory.get(item, 0) + quantity
print(f"Added {quantity} {item}(s). Stock: {inventory[item]}")
def sell_item(inventory, item, quantity):
stock = inventory.get(item, 0)
if stock == 0:
print(f"'{item}' not in stock!")
return
if quantity > stock:
print(f"Only {stock} {item}(s) available!")
return
inventory[item] -= quantity
print(f"Sold {quantity} {item}(s). Remaining: {inventory[item]}")
def check_stock(inventory, item):
return inventory.get(item, 0)
def low_stock(inventory, threshold):
return {item: qty for item, qty in inventory.items() if qty <= threshold}
# Test
inv = {}
add_item(inv, "apples", 50)
add_item(inv, "bananas", 10)
add_item(inv, "apples", 20) # Now 70
sell_item(inv, "apples", 15) # Now 55
sell_item(inv, "oranges", 5) # Not in stock!
print(f"Apple stock: {check_stock(inv, 'apples')}")
print(f"Low stock: {low_stock(inv, 15)}")
function addItem(inventory, item, quantity) {
inventory[item] = (inventory[item] ?? 0) + quantity;
console.log(`Added ${quantity} ${item}(s). Stock: ${inventory[item]}`);
}
function sellItem(inventory, item, quantity) {
let stock = inventory[item] ?? 0;
if (stock === 0) { console.log(`'${item}' not in stock!`); return; }
if (quantity > stock) { console.log(`Only ${stock} ${item}(s) available!`); return; }
inventory[item] -= quantity;
console.log(`Sold ${quantity} ${item}(s). Remaining: ${inventory[item]}`);
}
function checkStock(inventory, item) {
return inventory[item] ?? 0;
}
function lowStock(inventory, threshold) {
return Object.fromEntries(
Object.entries(inventory).filter(([_, qty]) => qty <= threshold)
);
}
let inv = {};
addItem(inv, "apples", 50);
addItem(inv, "bananas", 10);
addItem(inv, "apples", 20);
sellItem(inv, "apples", 15);
console.log(`Low stock:`, lowStock(inv, 15));
static void AddItem(Dictionary<string, int> inv, string item, int qty)
{
inv[item] = inv.GetValueOrDefault(item, 0) + qty;
Console.WriteLine($"Added {qty} {item}(s). Stock: {inv[item]}");
}
static void SellItem(Dictionary<string, int> inv, string item, int qty)
{
int stock = inv.GetValueOrDefault(item, 0);
if (stock == 0) { Console.WriteLine($"'{item}' not in stock!"); return; }
if (qty > stock) { Console.WriteLine($"Only {stock} {item}(s) available!"); return; }
inv[item] -= qty;
Console.WriteLine($"Sold {qty} {item}(s). Remaining: {inv[item]}");
}
static int CheckStock(Dictionary<string, int> inv, string item)
=> inv.GetValueOrDefault(item, 0);
static Dictionary<string, int> LowStock(Dictionary<string, int> inv, int threshold)
=> inv.Where(kvp => kvp.Value <= threshold)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
// Test
var inv = new Dictionary<string, int>();
AddItem(inv, "apples", 50);
AddItem(inv, "bananas", 10);
AddItem(inv, "apples", 20);
SellItem(inv, "apples", 15);
🏋️ Exercise 3: Contact Book
Objective: Build a contact book where each contact has a name (key) and details (nested dict/object with phone, email, city). Implement:
add_contact(book, name, details)— add or update a contactfind_contact(book, name)— look up a contactsearch_by_city(book, city)— find all contacts in a citylist_all(book)— display all contacts formatted nicely
✅ Solution
def add_contact(book, name, details):
book[name] = details
print(f"{'Updated' if name in book else 'Added'}: {name}")
def find_contact(book, name):
contact = book.get(name)
if contact is None:
print(f"'{name}' not found.")
return None
print(f"{name}:")
for key, val in contact.items():
print(f" {key}: {val}")
return contact
def search_by_city(book, city):
city_lower = city.lower()
matches = {name: info for name, info in book.items()
if info.get("city", "").lower() == city_lower}
print(f"Contacts in {city}: {list(matches.keys())}")
return matches
def list_all(book):
if not book:
print("Contact book is empty.")
return
for name, info in book.items():
print(f"\n{name}")
for key, val in info.items():
print(f" {key}: {val}")
# Test
contacts = {}
add_contact(contacts, "Alice", {"phone": "555-0101", "email": "alice@test.com", "city": "Boston"})
add_contact(contacts, "Bob", {"phone": "555-0202", "email": "bob@test.com", "city": "NYC"})
add_contact(contacts, "Charlie", {"phone": "555-0303", "email": "charlie@test.com", "city": "Boston"})
find_contact(contacts, "Alice")
search_by_city(contacts, "Boston")
list_all(contacts)
function addContact(book, name, details) {
let action = name in book ? "Updated" : "Added";
book[name] = details;
console.log(`${action}: ${name}`);
}
function findContact(book, name) {
let contact = book[name];
if (!contact) { console.log(`'${name}' not found.`); return null; }
console.log(`${name}:`);
for (let [key, val] of Object.entries(contact)) {
console.log(` ${key}: ${val}`);
}
return contact;
}
function searchByCity(book, city) {
let cl = city.toLowerCase();
let matches = Object.fromEntries(
Object.entries(book).filter(([_, info]) =>
(info.city ?? "").toLowerCase() === cl)
);
console.log(`Contacts in ${city}:`, Object.keys(matches));
return matches;
}
function listAll(book) {
if (Object.keys(book).length === 0) { console.log("Empty."); return; }
for (let [name, info] of Object.entries(book)) {
console.log(`\n${name}`);
for (let [key, val] of Object.entries(info))
console.log(` ${key}: ${val}`);
}
}
let contacts = {};
addContact(contacts, "Alice", { phone: "555-0101", email: "alice@test.com", city: "Boston" });
addContact(contacts, "Bob", { phone: "555-0202", email: "bob@test.com", city: "NYC" });
addContact(contacts, "Charlie", { phone: "555-0303", email: "charlie@test.com", city: "Boston" });
searchByCity(contacts, "Boston");
listAll(contacts);
// Using Dictionary<string, Dictionary<string, string>>
var contacts = new Dictionary<string, Dictionary<string, string>>();
static void AddContact(Dictionary<string, Dictionary<string, string>> book,
string name, Dictionary<string, string> details)
{
string action = book.ContainsKey(name) ? "Updated" : "Added";
book[name] = details;
Console.WriteLine($"{action}: {name}");
}
static void FindContact(Dictionary<string, Dictionary<string, string>> book,
string name)
{
if (!book.TryGetValue(name, out var contact))
{ Console.WriteLine($"'{name}' not found."); return; }
Console.WriteLine($"{name}:");
foreach (var kvp in contact)
Console.WriteLine($" {kvp.Key}: {kvp.Value}");
}
static void SearchByCity(Dictionary<string, Dictionary<string, string>> book,
string city)
{
string cl = city.ToLower();
var matches = book.Where(kvp =>
kvp.Value.GetValueOrDefault("city", "").ToLower() == cl)
.Select(kvp => kvp.Key);
Console.WriteLine($"Contacts in {city}: {string.Join(", ", matches)}");
}
// Test
AddContact(contacts, "Alice",
new() { {"phone","555-0101"}, {"email","alice@test.com"}, {"city","Boston"} });
AddContact(contacts, "Bob",
new() { {"phone","555-0202"}, {"email","bob@test.com"}, {"city","NYC"} });
FindContact(contacts, "Alice");
SearchByCity(contacts, "Boston");
🎓 Instructor Note: Delivery Guidance
Exercise 1 (word counter) is a classic dict exercise — the .get(word, 0) + 1 pattern is so important it deserves extra attention. Walk through the first few iterations by hand on a whiteboard. Exercise 2 combines dicts with validation logic from Module 3. Exercise 3 introduces nested dicts in a practical context and previews Module 6 (OOP) — the contacts "want" to be objects. Point that out as motivation for what's coming next. Challenge fast students: add a "delete contact" and "export as formatted string" function.
Summary
🎉 Key Takeaways
- Key-value collections store data by name instead of position: Python
dict, JSobject, C#Dictionary<K,V> - Access: Python
d["key"]or.get(), JSobj.keyorobj["key"], C#d["key"]orTryGetValue() - Same syntax adds and updates:
d["key"] = valueworks whether the key exists or not - Safe access is important — Python's
.get(), JS's??and?., C#'sTryGetValue() - JavaScript has both objects (string keys, structured data) and Maps (any key type, dynamic lookups)
- Nested structures model complex real-world data — and they're the foundation for working with JSON
- Iteration: Python
.items(), JSObject.entries(), C#foreachwithKeyValuePair
🚀 What's Next?
You've now seen both major collection types: ordered lists (Lesson 13) and keyed dictionaries (this lesson). Next up: iterating over collections — the patterns, methods, and techniques for processing every element in a collection efficiently.
🎯 Quick Check
Question 1: What happens in Python when you access a dictionary key that doesn't exist using bracket notation?
Question 2: In JavaScript, what's the difference between an object and a Map?