Skip to main content

🏷️ 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 collections module
  • 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).

🏷️ Dictionary: Keys β†’ Values "name" "Alice" "age" 25 "city" "NYC" "job" "Developer" person = {"name": "Alice", "age": 25, "city": "NYC", "job": "Developer"}

πŸ› οΈ 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)
graph LR A["🏷️ Dict Creation"] --> B["πŸ“ Literal
{'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:

🧰 Dictionary Operations Add / Update: dict["key"] = value Delete: del dict["key"] Pop (remove & return): dict.pop("key") Update multiple: dict.update({...}) Clear all: dict.clear() Adding a key that already exists overwrites the old value Dictionary keys must be immutable (strings, numbers, tuples)
# 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})")
graph TD A["πŸ”„ Looping Methods"] --> B["for key in dict
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:

graph TD A["🏫 School Database"] --> B["πŸ‘© student1"] A --> C["πŸ‘¨ student2"] B --> D["name: Alice"] B --> E["πŸ“Š scores"] E --> F["math: 95"] E --> G["science: 88"] C --> H["name: Bob"] C --> I["πŸ“Š scores"]
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}
graph LR A["{key: value"] --> B["for k, v in iterable"] B --> C["if condition}"] style A fill:#eff6ff,stroke:#3b82f6 style B fill:#f0fdf4,stroke:#22c55e style C fill:#fef9c3,stroke:#eab308

πŸ§ͺ 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 byIndex (number)Key (label)
Best forOrdered sequencesLabeled/lookup data
DuplicatesAllowedKeys must be unique
Example[1, 2, 3, 4]{"a": 1, "b": 2}
Lookup speedO(n) β€” must scanO(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:

  1. Take a paragraph of text as input
  2. Use Counter to count word frequencies
  3. Display the 5 most common words
  4. 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:

  1. Use defaultdict(list) to store each student's grades
  2. Add grades for multiple students across subjects
  3. Calculate each student's average
  4. 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:

  1. Given the mock search results below, find the highest-rated product
  2. Calculate the average price
  3. 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
  • defaultdict eliminates key-existence checks; Counter makes 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!