π·οΈ Lesson 8: Dictionaries β Data with Labels
If lists are like numbered lockers, dictionaries are like labeled drawers. Instead of remembering that your socks are in drawer #3, you just look for the drawer labeled "socks"!
π― Learning Objectives
By the end of this lesson, you will be able to:
- Create dictionaries and access values by key
- Add, update, and remove key-value pairs
- Loop through dictionaries using
.keys(),.values(), and.items() - Work with nested dictionaries for complex data
- Use dictionary comprehensions and the
collectionsmodule - Read and write JSON data
Estimated Time: 55β70 minutes
Project: Build a Contact Book with JSON persistence
In This Lesson
π What Are Dictionaries?
A dictionary stores data as key-value pairs. Every value has a meaningful label (the key) instead of just a number. Think of a real dictionary: you look up a word (key) to find its definition (value).
π οΈ Creating Dictionaries
Dictionaries use curly braces {} with key-value pairs separated by colons:
# Empty dictionary
inventory = {}
# Dictionary with data
person = {
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"is_student": False
}
# Different creation methods
# Method 1: Direct creation
colors = {"red": "#FF0000", "green": "#00FF00", "blue": "#0000FF"}
# Method 2: From a list of pairs
pairs = [("apple", 3), ("banana", 5), ("orange", 2)]
fruit_count = dict(pairs)
# Method 3: Using dict() with keyword arguments
dog = dict(name="Buddy", breed="Golden Retriever", age=5)
{'k': 'v'}"] A --> C["π From pairs
dict(pairs)"] A --> D["π Keywords
dict(k='v')"]
π Accessing Values
Use keys β like looking up words in a real dictionary:
student = {
"name": "Sarah Chen",
"grade": 11,
"subjects": ["Math", "Science", "History"],
"gpa": 3.8
}
# Access with square brackets
print(student["name"]) # "Sarah Chen"
print(student["grade"]) # 11
# Access with get() β safer!
print(student.get("gpa")) # 3.8
print(student.get("phone")) # None (no error!)
print(student.get("phone", "N/A")) # "N/A" (custom default)
# Check if key exists
if "subjects" in student:
print(f"Taking {len(student['subjects'])} subjects")
β Pro Tip
Always prefer .get() over bracket access when a key might not exist. Bracket access raises a KeyError if the key is missing; .get() returns None (or your default) instead.
π§ Modifying Dictionaries
Dictionaries are mutable β add, update, or remove items at any time:
# Starting dictionary
contact = {"name": "Alice", "email": "alice@email.com"}
# Adding new items
contact["phone"] = "555-1234"
contact["city"] = "Boston"
# Updating existing items
contact["email"] = "alice.smith@email.com"
# Removing items
del contact["city"]
phone = contact.pop("phone") # Remove and return
# Update multiple at once
contact.update({
"job": "Engineer",
"company": "TechCorp",
"email": "alice@techcorp.com"
})
print(contact)
Output:
{'name': 'Alice', 'email': 'alice@techcorp.com', 'job': 'Engineer', 'company': 'TechCorp'}
π Looping Through Dictionaries
Several ways to iterate through dictionary data:
inventory = {
"apples": 10,
"bananas": 6,
"oranges": 8,
"grapes": 15
}
# Loop through keys (default)
print("We have:")
for fruit in inventory:
print(f" - {fruit}")
# Loop through values
total = 0
for count in inventory.values():
total += count
print(f"\nTotal fruits: {total}")
# Loop through both keys and values
print("\nInventory details:")
for fruit, count in inventory.items():
print(f" {fruit}: {count} pieces")
# With enumeration
print("\nNumbered list:")
for i, (fruit, count) in enumerate(inventory.items(), 1):
print(f" {i}. {fruit} ({count})")
Keys only"] A --> C["for v in dict.values()
Values only"] A --> D["for k, v in dict.items()
β¨ Both key + value"]
π Nested Dictionaries
Dictionaries can contain other dictionaries β perfect for complex, structured data:
school = {
"student1": {
"name": "Alice Johnson",
"grade": 10,
"scores": {"math": 95, "science": 88, "english": 92}
},
"student2": {
"name": "Bob Smith",
"grade": 11,
"scores": {"math": 78, "science": 85, "english": 90}
}
}
# Accessing nested data
print(school["student1"]["name"]) # "Alice Johnson"
print(school["student1"]["scores"]["math"]) # 95
# Adding to nested structure
school["student3"] = {
"name": "Carol Davis",
"grade": 10,
"scores": {"math": 88, "science": 92, "english": 85}
}
# Calculate average for a student
alice_scores = school["student1"]["scores"]
average = sum(alice_scores.values()) / len(alice_scores)
print(f"Alice's average: {average:.1f}")
Output:
Alice Johnson
95
Alice's average: 91.7
π Real-World Example
API responses from weather services, GitHub, or online stores all use nested dictionary structures. Learning to navigate nested data is one of the most practical skills you'll gain!
β¨ Dictionary Comprehensions
Just like list comprehensions, but for dictionaries:
# Basic dictionary comprehension
squares = {x: x**2 for x in range(1, 6)}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# With a condition
even_squares = {x: x**2 for x in range(10) if x % 2 == 0}
print(even_squares) # {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
# Transform an existing dictionary
prices = {"apple": 1.5, "banana": 0.8, "orange": 2.0, "grape": 3.5}
sale_prices = {item: price * 0.8 for item, price in prices.items() if price > 1}
print(sale_prices) # {'apple': 1.2, 'orange': 1.6, 'grape': 2.8}
π§ͺ Advanced: defaultdict and Counter
Python's collections module has powerful dictionary variants that save you tons of boilerplate:
defaultdict β Never Worry About Missing Keys
from collections import defaultdict
# Regular dict β must check if key exists
regular_dict = {}
text = "hello world"
for letter in text:
if letter in regular_dict:
regular_dict[letter] += 1
else:
regular_dict[letter] = 1
# defaultdict β much cleaner!
letter_count = defaultdict(int) # default value is 0
for letter in text:
letter_count[letter] += 1
print(dict(letter_count))
# defaultdict with list β auto-creates empty lists
grouped = defaultdict(list)
students = [
("Alice", "Math"), ("Bob", "Math"),
("Alice", "Science"), ("Carol", "Math")
]
for student, subject in students:
grouped[student].append(subject)
print(dict(grouped))
# {'Alice': ['Math', 'Science'], 'Bob': ['Math'], 'Carol': ['Math']}
Counter β Count Anything Easily
from collections import Counter
# Count letters
letter_counts = Counter("mississippi")
print(letter_counts) # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
# Most common elements
print(letter_counts.most_common(2)) # [('i', 4), ('s', 4)]
# Count words
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
word_counts = Counter(words)
print(word_counts) # Counter({'apple': 3, 'banana': 2, 'orange': 1})
# Combine counters
counter1 = Counter(['a', 'b', 'c', 'a'])
counter2 = Counter(['a', 'b', 'b', 'd'])
combined = counter1 + counter2
print(combined) # Counter({'a': 3, 'b': 3, 'c': 1, 'd': 1})
π Working with JSON
JSON (JavaScript Object Notation) is the standard format for web APIs. Python dictionaries translate perfectly to JSON!
import json
# Dictionary β JSON string
person = {
"name": "Alice Johnson",
"age": 28,
"hobbies": ["reading", "hiking", "photography"],
"is_employed": True
}
json_string = json.dumps(person, indent=2)
print(json_string)
# JSON string β Dictionary
json_data = '{"product": "laptop", "price": 999.99, "in_stock": true}'
product = json.loads(json_data)
print(product["price"]) # 999.99
# Save to a file
with open("data.json", "w") as file:
json.dump(person, file, indent=2)
# Load from a file
with open("data.json", "r") as file:
loaded = json.load(file)
print(loaded["name"]) # Alice Johnson
π Real API Response Example
Here's what navigating a nested API response looks like in practice:
weather = {
"location": {"name": "San Francisco", "country": "USA"},
"current": {
"temp_c": 18.5,
"condition": {"text": "Partly cloudy"},
"humidity": 68
}
}
city = weather["location"]["name"]
temp = weather["current"]["temp_c"]
cond = weather["current"]["condition"]["text"]
print(f"{city}: {temp}Β°C, {cond}")
# San Francisco: 18.5Β°C, Partly cloudy
π§© Common Patterns and Mistakes
Useful Patterns
# Pattern 1: Dictionary as a switch/lookup
def get_day_name(day_num):
days = {
1: "Monday", 2: "Tuesday", 3: "Wednesday",
4: "Thursday", 5: "Friday", 6: "Saturday", 7: "Sunday"
}
return days.get(day_num, "Invalid day")
# Pattern 2: Caching results
cache = {}
def expensive_operation(n):
if n not in cache:
cache[n] = sum(i**2 for i in range(n))
return cache[n]
# Pattern 3: Counting with Counter
from collections import Counter
votes = ["Alice", "Bob", "Alice", "Carol", "Bob", "Alice"]
election = Counter(votes)
winner = election.most_common(1)[0]
print(f"Winner: {winner[0]} with {winner[1]} votes")
# Pattern 4: Merging dictionaries (Python 3.9+)
dict1 = {"a": 1, "b": 2}
dict2 = {"b": 3, "c": 4}
merged = dict1 | dict2 # {"a": 1, "b": 3, "c": 4}
β οΈ Mistake: KeyError from Bracket Access
# Wrong
person = {"name": "Alice", "age": 25}
# print(person["email"]) # KeyError!
# Right β use get() or check first
print(person.get("email", "No email"))
if "email" in person:
print(person["email"])
β οΈ Mistake: Modifying While Iterating
# WRONG
scores = {"Alice": 95, "Bob": 78, "Carol": 88}
for name in scores:
if scores[name] < 80:
del scores[name] # RuntimeError!
# RIGHT β iterate over a copy of the keys
for name in list(scores.keys()):
if scores[name] < 80:
del scores[name]
# Or use a comprehension
scores = {n: s for n, s in scores.items() if s >= 80}
π Dictionary vs List β When to Use Which?
| Feature | List | Dictionary |
|---|---|---|
| Access by | Index (number) | Key (label) |
| Best for | Ordered sequences | Labeled/lookup data |
| Duplicates | Allowed | Keys must be unique |
| Example | [1, 2, 3, 4] | {"a": 1, "b": 2} |
| Lookup speed | O(n) β must scan | O(1) β instant by key |
π Project: Contact Book
This project combines dictionaries, loops, JSON, and everything you've learned. It saves contacts to a file so they persist between runs:
import json
FILENAME = "contacts.json"
def load_contacts():
"""Load contacts from JSON file."""
try:
with open(FILENAME, "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
def save_contacts(contacts):
"""Save contacts to JSON file."""
with open(FILENAME, "w") as f:
json.dump(contacts, f, indent=2)
def add_contact(contacts):
name = input("Name: ")
phone = input("Phone: ")
email = input("Email (or press Enter to skip): ") or "N/A"
contacts[name] = {"phone": phone, "email": email}
save_contacts(contacts)
print(f"β
Added {name}!")
def search_contact(contacts):
name = input("Search name: ")
if name in contacts:
info = contacts[name]
print(f"\nπ {name}")
print(f" Phone: {info['phone']}")
print(f" Email: {info['email']}")
else:
# Partial match search
matches = {n: c for n, c in contacts.items()
if name.lower() in n.lower()}
if matches:
print(f"\nPartial matches:")
for n, c in matches.items():
print(f" {n}: {c['phone']}")
else:
print("No contacts found.")
def list_contacts(contacts):
if not contacts:
print("Contact book is empty.")
return
print(f"\nπ All Contacts ({len(contacts)}):")
for i, (name, info) in enumerate(sorted(contacts.items()), 1):
print(f" {i}. {name} β {info['phone']}")
def delete_contact(contacts):
name = input("Name to delete: ")
if name in contacts:
del contacts[name]
save_contacts(contacts)
print(f"ποΈ Deleted {name}.")
else:
print("Contact not found.")
# Main loop
contacts = load_contacts()
print("π Contact Book")
print("=" * 30)
while True:
print("\n1. Add 2. Search 3. List 4. Delete 5. Quit")
choice = input("Choice: ")
if choice == "1":
add_contact(contacts)
elif choice == "2":
search_contact(contacts)
elif choice == "3":
list_contacts(contacts)
elif choice == "4":
delete_contact(contacts)
elif choice == "5":
print("Goodbye! π")
break
else:
print("Invalid choice.")
ποΈ Practice Exercises
ποΈ Exercise 1: Word Frequency Analyzer
Objective: Count word frequencies in a paragraph, find the 5 most common words, and save results to JSON.
Instructions:
- Take a paragraph of text as input
- Use
Counterto count word frequencies - Display the 5 most common words
- Use a comprehension to filter words longer than 4 letters
π‘ Hint
Use .lower().split() to break the text into words. Counter from collections has a .most_common(n) method.
β Solution
from collections import Counter
import json
text = input("Enter a paragraph: ")
words = text.lower().split()
# Count frequencies
word_counts = Counter(words)
# Top 5
print("\nπ Top 5 most common words:")
for word, count in word_counts.most_common(5):
print(f" '{word}': {count} times")
# Filter long words
long_words = {w: c for w, c in word_counts.items() if len(w) > 4}
print(f"\nWords longer than 4 letters: {long_words}")
# Save to JSON
with open("word_freq.json", "w") as f:
json.dump(dict(word_counts), f, indent=2)
print("Results saved to word_freq.json")
ποΈ Exercise 2: Grade Book with Statistics
Objective: Build a grade tracking system using defaultdict that calculates class statistics.
Instructions:
- Use
defaultdict(list)to store each student's grades - Add grades for multiple students across subjects
- Calculate each student's average
- Find the class average and top student
π‘ Hint
With defaultdict(list), you can do grades[name].append(score) without checking if the key exists. Use sum()/len() for averages.
β Solution
from collections import defaultdict
grades = defaultdict(list)
# Add grades
data = [
("Alice", 95), ("Alice", 88), ("Alice", 92),
("Bob", 78), ("Bob", 85), ("Bob", 80),
("Carol", 90), ("Carol", 95), ("Carol", 88)
]
for student, score in data:
grades[student].append(score)
# Student averages
print("π Student Averages:")
averages = {}
for student, scores in grades.items():
avg = sum(scores) / len(scores)
averages[student] = avg
print(f" {student}: {avg:.1f}")
# Class average
class_avg = sum(averages.values()) / len(averages)
print(f"\nClass average: {class_avg:.1f}")
# Top student
top = max(averages, key=averages.get)
print(f"Top student: {top} ({averages[top]:.1f})")
ποΈ Exercise 3: API Response Processor
Objective: Process a mock API response β extract, filter, and summarize nested dictionary data.
Instructions:
- Given the mock search results below, find the highest-rated product
- Calculate the average price
- Use a comprehension to get only products over $25
π‘ Hint
Use max(items, key=lambda x: x["rating"]) for the highest-rated item. Access nested data step by step.
β Solution
search_results = {
"total_count": 3,
"items": [
{"id": 1, "name": "Product A", "price": 29.99, "rating": 4.5},
{"id": 2, "name": "Product B", "price": 39.99, "rating": 4.8},
{"id": 3, "name": "Product C", "price": 19.99, "rating": 4.2}
]
}
items = search_results["items"]
# Highest rated
best = max(items, key=lambda x: x["rating"])
print(f"Highest rated: {best['name']} ({best['rating']} stars)")
# Average price
avg_price = sum(i["price"] for i in items) / len(items)
print(f"Average price: ${avg_price:.2f}")
# Filter expensive items
expensive = {i["name"]: i["price"] for i in items if i["price"] > 25}
print(f"Over $25: {expensive}")
π― Quick Quiz
Question 1: What does person.get("email", "N/A") return if the key "email" does not exist?
Question 2: Which method gives you both keys and values when looping?
Question 3: What does json.dumps() do?
π Summary
π Key Takeaways
- Dictionaries store key-value pairs for fast, labeled lookups
- Use
.get()for safe access with default values - Loop with
.items()to get both keys and values - Nested dictionaries model complex, real-world data
- Dictionary comprehensions offer powerful one-line transformations
defaultdicteliminates key-existence checks;Countermakes counting trivial- JSON and dictionaries are natural partners β essential for web APIs
- Choose dictionaries for labeled data, lists for ordered sequences
π Real-World Applications
Dictionaries power configuration files (JSON/YAML settings), API responses (RESTful web services), caching (storing computed results for quick lookup), database records (NoSQL document structures), game development (player stats, inventory, game state), and data processing (grouping, counting, and transforming datasets).
π What's Next?
Now that you can store and organize data with lists and dictionaries, we'll learn about functions β how to create your own reusable commands. Functions let you write code once and use it everywhere!
π Congratulations!
You've completed Module 4: Data Structures! You now have two of Python's most powerful tools in your belt β lists for sequences and dictionaries for labeled data. Onward to functions!