Python & AI Tutorials Logo
Python Programming

16. Dictionaries: Mapping Keys to Values

In the previous chapters, we learned about lists and tuples—collections that store items in a specific order and let us access them by position. But what if we want to look up information by something more meaningful than a number? What if we want to find a student's grade by their name, or a product's price by its ID, or a word's definition by the word itself?

This is where dictionaries come in. A dictionary is Python's built-in data structure for storing key-value pairs. Instead of accessing items by their position (like grades[0]), we access them by their key (like grades["Alice"]). This makes dictionaries incredibly powerful for organizing and retrieving data in real-world programs.

Think of a dictionary like a real-world dictionary or phone book: you look up a word (the key) to find its definition (the value), or you look up a name to find a phone number. Python dictionaries work the same way—they map keys to values, allowing fast lookups and flexible data organization.

16.1) Creating Dictionaries and Accessing Values

16.1.1) What Is a Dictionary?

A dictionary is a collection of key-value pairs. Each key is associated with a value, and you use the key to retrieve the value. Keys must be unique within a dictionary—you can't have two entries with the same key. Values, however, can be duplicated.

Here's the basic structure:

  • Keys: Unique identifiers used to look up values (like names, IDs, or labels)
  • Values: The data associated with each key (like grades, prices, or descriptions)

Dictionaries are:

  • Mutable: You can add, modify, or remove key-value pairs after creation
  • Unordered (in Python 3.6 and earlier) or insertion-ordered (in Python 3.7+): While modern Python preserves the order in which you add items, you should think of dictionaries as collections where you access items by key, not by position
  • Dynamic: They can grow and shrink as needed

16.1.2) Creating Empty and Simple Dictionaries

The simplest way to create a dictionary is using curly braces {} with key-value pairs separated by colons:

python
# Empty dictionary
empty_dict = {}
print(empty_dict)  # Output: {}
print(type(empty_dict))  # Output: <class 'dict'>
 
# Dictionary with student grades
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# Dictionary with product prices
prices = {"apple": 0.50, "banana": 0.30, "orange": 0.75}
print(prices)  # Output: {'apple': 0.5, 'banana': 0.3, 'orange': 0.75}

Notice the syntax: each key-value pair is written as key: value, and pairs are separated by commas. The keys here are strings ("Alice", "apple"), and the values are numbers, but both keys and values can be many different types.

You can also create a dictionary using the dict() constructor:

python
# Using dict() with keyword arguments
student = dict(name="Alice", age=20, major="Computer Science")
print(student)  # Output: {'name': 'Alice', 'age': 20, 'major': 'Computer Science'}
 
# Using dict() with a list of tuples
colors = dict([("red", "#FF0000"), ("green", "#00FF00"), ("blue", "#0000FF")])
print(colors)  # Output: {'red': '#FF0000', 'green': '#00FF00', 'blue': '#0000FF'}

The dict() constructor is useful when you're building dictionaries from other data structures or when you want to use Python identifiers as keys (without quotes).

16.1.3) Accessing Values by Key

To retrieve a value from a dictionary, use square brackets with the key:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Access individual values
alice_grade = grades["Alice"]
print(alice_grade)  # Output: 95
 
bob_grade = grades["Bob"]
print(bob_grade)  # Output: 87

This is the most direct way to access dictionary values. However, if you try to access a key that doesn't exist, Python raises a KeyError:

python
grades = {"Alice": 95, "Bob": 87}
 
# WARNING: KeyError - for demonstration only
# print(grades["David"])  # PROBLEM: KeyError: 'David'

This error occurs because "David" is not a key in the dictionary. We'll learn how to handle this safely in the next subsection.

16.1.4) Safe Access with get()

To avoid KeyError when a key might not exist, use the get() method. It returns None (or a default value you specify) if the key isn't found:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Safe access with get()
alice_grade = grades.get("Alice")
print(alice_grade)  # Output: 95
 
# Key doesn't exist - returns None
david_grade = grades.get("David")
print(david_grade)  # Output: None
 
# Provide a default value
david_grade = grades.get("David", 0)
print(david_grade)  # Output: 0
 
# Using get() in conditional logic
if grades.get("Eve") is None:
    print("Eve is not in the grade book")  # Output: Eve is not in the grade book

The get() method is safer than direct bracket access when you're not certain a key exists. The second argument to get() is the default value to return if the key is missing—if you don't provide one, it defaults to None.

Here's a practical example showing when get() is useful:

python
# Student database with optional information
students = {
    "Alice": {"age": 20, "major": "CS"},
    "Bob": {"age": 19},  # Bob hasn't declared a major yet
    "Charlie": {"major": "Math"}  # Charlie's age not recorded
}
 
# Safely access potentially missing information
for name in ["Alice", "Bob", "Charlie"]:
    student = students[name]
    age = student.get("age", "Unknown")
    major = student.get("major", "Undeclared")
    print(f"{name}: Age {age}, Major {major}")
 
# Output:
# Alice: Age 20, Major CS
# Bob: Age 19, Major Undeclared
# Charlie: Age Unknown, Major Math

16.1.5) Valid Key Types

Dictionary keys must be hashable—a technical term meaning the key's value cannot change. In practice, this means:

Valid key types (immutable):

  • Strings: "name", "id_123"
  • Numbers: 42, 3.14
  • Tuples (if they contain only immutable items): (1, 2), ("x", "y")
  • Booleans: True, False
  • None

Invalid key types (mutable):

  • Lists: [1, 2, 3] cannot be a key
  • Dictionaries: {"a": 1} cannot be a key
  • Sets: {1, 2, 3} cannot be a key
python
# Valid keys
valid_dict = {
    "name": "Alice",           # String key
    42: "answer",              # Integer key
    3.14: "pi",                # Float key
    (1, 2): "coordinates",     # Tuple key
    True: "yes",               # Boolean key
    None: "nothing"            # None key
}
print(valid_dict["name"])      # Output: Alice
print(valid_dict[42])          # Output: answer
print(valid_dict[(1, 2)])      # Output: coordinates
 
# WARNING: Invalid keys - for demonstration only
# invalid_dict = {[1, 2]: "list key"}  # PROBLEM: TypeError: unhashable type: 'list'
# invalid_dict = {{1, 2}: "set key"}   # PROBLEM: TypeError: unhashable type: 'set'

Common Beginner Mistake: A frequent error is trying to use a list as a dictionary key because it seems logical. For example, you might want to use coordinates [x, y] as a key to store location data. When Python raises TypeError: unhashable type: 'list', beginners often don't understand why—after all, the list contains the exact data they want to use as a key.

The reason is that lists are mutable (they can change), and Python needs dictionary keys to be stable and unchangeable. If you need to use something list-like as a key, convert it to a tuple first: tuple([1, 2]) becomes (1, 2), which can be used as a key. Tuples are immutable, so they work perfectly:

python
# Wrong: trying to use a list as a key
# locations = {[0, 0]: "origin", [1, 0]: "east"}  # PROBLEM: TypeError
 
# Right: convert to tuple
locations = {(0, 0): "origin", (1, 0): "east", (0, 1): "north"}
print(locations[(0, 0)])  # Output: origin
print(locations[(1, 0)])  # Output: east

Values, on the other hand, can be any type—mutable or immutable:

python
# Values can be any type
flexible_dict = {
    "numbers": [1, 2, 3],              # List value
    "nested": {"a": 1, "b": 2},        # Dictionary value
    "function": len,                    # Function value
    "mixed": (1, [2, 3], {"x": 4})     # Tuple containing mutable items
}
print(flexible_dict["numbers"])        # Output: [1, 2, 3]
print(flexible_dict["nested"]["a"])    # Output: 1

We'll explore hashability in more depth in Chapter 17, but for now, remember: use immutable types (strings, numbers, tuples) as keys, and you'll be fine.

16.2) Adding and Updating Dictionary Entries

16.2.1) Adding New Key-Value Pairs

Adding a new entry to a dictionary is straightforward—just assign a value to a new key:

python
grades = {"Alice": 95, "Bob": 87}
print(grades)  # Output: {'Alice': 95, 'Bob': 87}
 
# Add a new student
grades["Charlie"] = 92
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# Add another student
grades["Diana"] = 88
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92, 'Diana': 88}

If the key doesn't exist, Python creates it. If it does exist, Python updates the value (which we'll cover next).

16.2.2) Updating Existing Values

To update a value, simply assign a new value to an existing key:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# Update Bob's grade
grades["Bob"] = 90
print(grades)  # Output: {'Alice': 95, 'Bob': 90, 'Charlie': 92}
 
# Update multiple grades
grades["Alice"] = 98
grades["Charlie"] = 94
print(grades)  # Output: {'Alice': 98, 'Bob': 90, 'Charlie': 94}

Python doesn't distinguish between adding and updating—the syntax is identical. If the key exists, the value is updated; if not, a new entry is created.

Here's a practical example showing both adding and updating:

python
# Track inventory
inventory = {"apple": 50, "banana": 30}
print("Initial inventory:", inventory)  # Output: Initial inventory: {'apple': 50, 'banana': 30}
 
# Restock apples (update existing)
inventory["apple"] = inventory["apple"] + 20
print("After restocking apples:", inventory)  # Output: After restocking apples: {'apple': 70, 'banana': 30}
 
# Add new product (add new key)
inventory["orange"] = 40
print("After adding oranges:", inventory)  # Output: After adding oranges: {'apple': 70, 'banana': 30, 'orange': 40}
 
# Sell some bananas (update existing)
inventory["banana"] = inventory["banana"] - 10
print("After selling bananas:", inventory)  # Output: After selling bananas: {'apple': 70, 'banana': 20, 'orange': 40}

16.2.3) Using update() to Merge Dictionaries

The update() method adds multiple key-value pairs at once or merges another dictionary into the current one:

python
grades = {"Alice": 95, "Bob": 87}
print(grades)  # Output: {'Alice': 95, 'Bob': 87}
 
# Add multiple students at once
new_students = {"Charlie": 92, "Diana": 88}
grades.update(new_students)
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92, 'Diana': 88}
 
# Update existing and add new
more_updates = {"Bob": 90, "Eve": 85}  # Bob's grade changes, Eve is new
grades.update(more_updates)
print(grades)  # Output: {'Alice': 95, 'Bob': 90, 'Charlie': 92, 'Diana': 88, 'Eve': 85}

The update() method modifies the dictionary in place. If a key already exists, its value is updated; if not, the key-value pair is added.

You can also pass keyword arguments to update():

python
student = {"name": "Alice", "age": 20}
print(student)  # Output: {'name': 'Alice', 'age': 20}
 
# Update using keyword arguments
student.update(age=21, major="Computer Science")
print(student)  # Output: {'name': 'Alice', 'age': 21, 'major': 'Computer Science'}

Here's a practical example merging configuration settings:

python
# Default settings
config = {
    "theme": "light",
    "font_size": 12,
    "auto_save": True
}
 
# User preferences (override some defaults)
user_prefs = {
    "theme": "dark",
    "font_size": 14
}
 
# Merge user preferences into config
config.update(user_prefs)
print(config)  # Output: {'theme': 'dark', 'font_size': 14, 'auto_save': True}

16.2.4) Using setdefault() to Add Only If Key Doesn't Exist

The setdefault() method is useful when you want to add a key-value pair only if the key doesn't already exist. If the key exists, it returns the current value without changing it:

python
grades = {"Alice": 95, "Bob": 87}
 
# Add Charlie (key doesn't exist)
result = grades.setdefault("Charlie", 90)
print(result)  # Output: 90
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 90}
 
# Try to add Alice (key exists - no change)
result = grades.setdefault("Alice", 80)
print(result)  # Output: 95 (existing value returned)
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 90} (unchanged)

This is particularly useful when you want to ensure all required configuration keys exist with default values, while preserving any values the user has already customized:

python
# Application configuration with defaults
config = {"theme": "light", "font_size": 12}
 
# Ensure all required settings exist with defaults
config.setdefault("auto_save", True)
config.setdefault("language", "en")
config.setdefault("theme", "dark")  # Won't change - already exists
 
print(config)
# Output: {'theme': 'light', 'font_size': 12, 'auto_save': True, 'language': 'en'}
 
# Now safely access all settings
print(f"Theme: {config['theme']}")           # Output: Theme: light
print(f"Auto-save: {config['auto_save']}")   # Output: Auto-save: True
print(f"Language: {config['language']}")     # Output: Language: en

16.3) Removing Dictionary Entries with del and pop()

16.3.1) Removing Entries with del

The del statement removes a key-value pair from a dictionary:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92, 'Diana': 88}
 
# Remove Charlie
del grades["Charlie"]
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Diana': 88}
 
# Remove another student
del grades["Bob"]
print(grades)  # Output: {'Alice': 95, 'Diana': 88}

If you try to delete a key that doesn't exist, Python raises a KeyError:

python
grades = {"Alice": 95, "Bob": 87}
 
# WARNING: KeyError - for demonstration only
# del grades["Charlie"]  # PROBLEM: KeyError: 'Charlie'

To safely delete a key that might not exist, check first:

python
grades = {"Alice": 95, "Bob": 87}
 
# Safe deletion
if "Charlie" in grades:
    del grades["Charlie"]
else:
    print("Charlie not found")  # Output: Charlie not found

16.3.2) Removing and Retrieving with pop()

The pop() method removes a key and returns its value. This is useful when you need to both remove an entry and use its value:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# Remove and get Bob's grade
bob_grade = grades.pop("Bob")
print(bob_grade)  # Output: 87
print(grades)  # Output: {'Alice': 95, 'Charlie': 92}
 
# Remove and use the value
charlie_grade = grades.pop("Charlie")
print(f"Charlie's final grade was {charlie_grade}")  # Output: Charlie's final grade was 92
print(grades)  # Output: {'Alice': 95}

Like del, pop() raises a KeyError if the key doesn't exist. However, you can provide a default value to return instead:

python
grades = {"Alice": 95, "Bob": 87}
 
# Pop with default value (key doesn't exist)
diana_grade = grades.pop("Diana", 0)
print(diana_grade)  # Output: 0
print(grades)  # Output: {'Alice': 95, 'Bob': 87} (unchanged)
 
# Pop with default value (key exists)
alice_grade = grades.pop("Alice", 0)
print(alice_grade)  # Output: 95
print(grades)  # Output: {'Bob': 87}

16.3.3) Removing All Entries with clear()

The clear() method removes all key-value pairs from a dictionary, leaving it empty:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(grades)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}
 
# Clear all entries
grades.clear()
print(grades)  # Output: {}
print(len(grades))  # Output: 0

This is more explicit than reassigning to an empty dictionary (grades = {}), especially when other variables reference the same dictionary:

python
# Demonstrate the difference
grades = {"Alice": 95, "Bob": 87}
backup = grades  # backup references the same dictionary
 
# Using clear() - affects both variables
grades.clear()
print(grades)   # Output: {}
print(backup)   # Output: {} (same dictionary was cleared)
 
# Reset for next example
grades = {"Alice": 95, "Bob": 87}
backup = grades
 
# Reassigning - only affects grades
grades = {}
print(grades)   # Output: {}
print(backup)   # Output: {'Alice': 95, 'Bob': 87} (different dictionary now)

We'll explore this behavior more in Chapter 18 when we discuss reference semantics, but for now, remember: clear() empties the existing dictionary, while reassignment creates a new empty dictionary.

16.4) Dictionary View Objects: keys(), values(), and items()

16.4.1) Understanding Dictionary Views

Dictionaries provide three methods that return view objects—special objects that provide a dynamic view of the dictionary's keys, values, or key-value pairs. These views reflect changes to the dictionary automatically:

  • keys(): Returns a view of all keys
  • values(): Returns a view of all values
  • items(): Returns a view of all key-value pairs (as tuples)
python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Get views
keys_view = grades.keys()
values_view = grades.values()
items_view = grades.items()
 
print(keys_view)    # Output: dict_keys(['Alice', 'Bob', 'Charlie'])
print(values_view)  # Output: dict_values([95, 87, 92])
print(items_view)   # Output: dict_items([('Alice', 95), ('Bob', 87), ('Charlie', 92)])

These are not lists—they're view objects. But you can convert them to lists if needed:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Convert views to lists
keys_list = list(grades.keys())
values_list = list(grades.values())
items_list = list(grades.items())
 
print(keys_list)    # Output: ['Alice', 'Bob', 'Charlie']
print(values_list)  # Output: [95, 87, 92]
print(items_list)   # Output: [('Alice', 95), ('Bob', 87), ('Charlie', 92)]

16.4.2) Views Are Dynamic

View objects reflect changes to the dictionary automatically:

python
grades = {"Alice": 95, "Bob": 87}
 
# Create a view
keys_view = grades.keys()
print(keys_view)  # Output: dict_keys(['Alice', 'Bob'])
 
# Modify the dictionary
grades["Charlie"] = 92
print(keys_view)  # Output: dict_keys(['Alice', 'Bob', 'Charlie'])
 
# Remove an entry
del grades["Bob"]
print(keys_view)  # Output: dict_keys(['Alice', 'Charlie'])

This dynamic behavior is useful when you need to work with dictionary contents that might change. However, if you need a snapshot that won't change, convert the view to a list:

python
grades = {"Alice": 95, "Bob": 87}
 
# Create a snapshot
keys_snapshot = list(grades.keys())
print(keys_snapshot)  # Output: ['Alice', 'Bob']
 
# Modify the dictionary
grades["Charlie"] = 92
print(keys_snapshot)  # Output: ['Alice', 'Bob'] (unchanged)

Dynamic Update

No Effect

Dictionary

View Object

List Snapshot

Modify Dictionary

16.4.3) Working with keys()

The keys() method returns a view of all dictionary keys. This is useful for checking what keys exist or iterating over them:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Get all keys
keys = grades.keys()
print(keys)  # Output: dict_keys(['Alice', 'Bob', 'Charlie'])
 
# Check if a key exists
if "Alice" in keys:
    print("Alice is in the grade book")  # Output: Alice is in the grade book
 
# Count keys
print(f"Number of students: {len(keys)}")  # Output: Number of students: 3

You can also check membership directly on the dictionary without calling keys():

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# These are equivalent
if "Alice" in grades.keys():
    print("Found (using keys())")
 
if "Alice" in grades:
    print("Found (direct check)")  # This is more common and concise
 
# Output:
# Found (using keys())
# Found (direct check)

16.4.4) Working with values()

The values() method returns a view of all dictionary values. This is useful when you need to process values without caring about their keys:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# Get all values
values = grades.values()
print(values)  # Output: dict_values([95, 87, 92, 88])
 
# Calculate statistics
total = sum(values)
count = len(values)
average = total / count
 
print(f"Total points: {total}")      # Output: Total points: 362
print(f"Number of students: {count}")  # Output: Number of students: 4
print(f"Average grade: {average}")   # Output: Average grade: 90.5
 
# Find highest and lowest grades
print(f"Highest grade: {max(values)}")  # Output: Highest grade: 95
print(f"Lowest grade: {min(values)}")   # Output: Lowest grade: 87

16.4.5) Working with items()

The items() method returns a view of key-value pairs as tuples. This is the most commonly used view because it gives you both keys and values:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Get all key-value pairs
items = grades.items()
print(items)  # Output: dict_items([('Alice', 95), ('Bob', 87), ('Charlie', 92)])
 
# Convert to list to see the tuples clearly
items_list = list(items)
print(items_list)  # Output: [('Alice', 95), ('Bob', 87), ('Charlie', 92)]
 
# Access individual tuples
first_item = items_list[0]
print(first_item)        # Output: ('Alice', 95)
print(first_item[0])     # Output: Alice
print(first_item[1])     # Output: 95

The items() view is particularly useful for iteration, which we'll cover in detail in the next section. Here's a preview:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Process each key-value pair
for name, grade in grades.items():
    print(f"{name}: {grade}")
 
# Output:
# Alice: 95
# Bob: 87
# Charlie: 92

16.5) Iterating Over Keys, Values, and Items

16.5.1) Iterating Over Keys (Default Behavior)

When you iterate over a dictionary directly with a for loop, you iterate over its keys:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Iterate over keys (implicit)
for name in grades:
    print(name)
 
# Output:
# Alice
# Bob
# Charlie

This is equivalent to iterating over grades.keys():

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Iterate over keys (explicit)
for name in grades.keys():
    print(name)
 
# Output:
# Alice
# Bob
# Charlie

Both approaches work identically. The implicit version (without .keys()) is more common and concise.

Here's a practical example using keys to access values:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# Find students who passed (grade >= 90)
passing_students = []
for name in grades:
    if grades[name] >= 90:
        passing_students.append(name)
 
print("Students who passed:", passing_students)  # Output: Students who passed: ['Alice', 'Charlie']

16.5.2) Iterating Over Values

To iterate over just the values, use values():

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# Iterate over values
for grade in grades.values():
    print(grade)
 
# Output:
# 95
# 87
# 92
# 88

This is useful when you need to process values but don't care which key they're associated with:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# Calculate total and average
total = 0
count = 0
for grade in grades.values():
    total = total + grade
    count = count + 1
 
average = total / count
print(f"Class average: {average}")  # Output: Class average: 90.5
 
# Check if all students passed
all_passed = True
for grade in grades.values():
    if grade < 60:
        all_passed = False
        break
 
if all_passed:
    print("All students passed!")  # Output: All students passed!

16.5.3) Iterating Over Key-Value Pairs with items()

The most common and useful iteration pattern is using items() to get both keys and values:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Iterate over key-value pairs
for name, grade in grades.items():
    print(f"{name} scored {grade}")
 
# Output:
# Alice scored 95
# Bob scored 87
# Charlie scored 92

Notice the tuple unpacking: for name, grade in grades.items(). Each item is a tuple like ("Alice", 95), and we unpack it into two variables. This is much more readable than accessing tuple indices:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Without unpacking (less readable)
for item in grades.items():
    print(f"{item[0]} scored {item[1]}")
 
# With unpacking (more readable)
for name, grade in grades.items():
    print(f"{name} scored {grade}")
 
# Both produce the same output:
# Alice scored 95
# Bob scored 87
# Charlie scored 92

16.5.4) Modifying Dictionaries During Iteration

Warning: Modifying a dictionary's size (adding or removing keys) while iterating over it can cause errors or unexpected behavior:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# WARNING: RuntimeError - for demonstration only
# for name in grades:
#     if grades[name] < 90:
#         del grades[name]  # PROBLEM: RuntimeError: dictionary changed size during iteration

In modern Python (3.7+), this immediately raises a RuntimeError as soon as you try to change the dictionary's size. Python detects the modification and stops execution to prevent unpredictable behavior.

In older Python versions, this could cause the iterator to:

  • Skip items it should process
  • Process the same item twice
  • Produce inconsistent results

This is why Python now fails fast with a clear error message.

If you need to modify a dictionary during iteration, iterate over a copy of the keys:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# Safe: iterate over a copy of keys
for name in list(grades.keys()):
    if grades[name] < 90:
        del grades[name]
 
print(grades)  # Output: {'Alice': 95, 'Charlie': 92}

Or build a new dictionary with only the entries you want:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
# Build a new dictionary with filtered entries
high_grades = {}
for name, grade in grades.items():
    if grade >= 90:
        high_grades[name] = grade
 
print(high_grades)  # Output: {'Alice': 95, 'Charlie': 92}

The second approach is often clearer and safer. We'll learn even more elegant ways to do this using dictionary comprehensions in Chapter 35.

16.6) Common Dictionary Methods

16.6.1) Checking for Keys with in and not in

The in and not in operators check if a key exists in a dictionary:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
 
# Check if keys exist
if "Alice" in grades:
    print("Alice is in the grade book")  # Output: Alice is in the grade book
 
if "David" not in grades:
    print("David is not in the grade book")  # Output: David is not in the grade book

This is the preferred way to check for key existence before accessing values. It's more readable and Pythonic than using get() and checking for None:

python
grades = {"Alice": 95, "Bob": 87}
 
# Preferred: using in
if "Alice" in grades:
    print(f"Alice's grade: {grades['Alice']}")  # Output: Alice's grade: 95
 
# Alternative: using get() and checking None
if grades.get("Alice") is not None:
    print(f"Alice's grade: {grades['Alice']}")  # Output: Alice's grade: 95

16.6.2) Getting the Number of Entries with len()

The len() function returns the number of key-value pairs in a dictionary:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
print(len(grades))  # Output: 3
 
# Empty dictionary
empty = {}
print(len(empty))  # Output: 0
 
# After modifications
grades["Diana"] = 88
print(len(grades))  # Output: 4
 
del grades["Bob"]
print(len(grades))  # Output: 3

This is useful for checking if a dictionary is empty or for reporting statistics:

python
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
 
if len(grades) == 0:
    print("No students in grade book")
else:
    total = sum(grades.values())
    average = total / len(grades)
    print(f"{len(grades)} students, average grade: {average}")
    # Output: 4 students, average grade: 90.5

16.6.3) Copying Dictionaries with copy()

The copy() method creates a shallow copy of a dictionary—a new dictionary with the same key-value pairs:

python
original = {"Alice": 95, "Bob": 87}
duplicate = original.copy()
 
print(original)   # Output: {'Alice': 95, 'Bob': 87}
print(duplicate)  # Output: {'Alice': 95, 'Bob': 87}
 
# Modify the copy
duplicate["Charlie"] = 92
print(original)   # Output: {'Alice': 95, 'Bob': 87} (unchanged)
print(duplicate)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}

This is different from simple assignment, which creates a reference to the same dictionary:

python
original = {"Alice": 95, "Bob": 87}
reference = original  # Not a copy - same dictionary
 
# Modify through reference
reference["Charlie"] = 92
print(original)   # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92} (changed!)
print(reference)  # Output: {'Alice': 95, 'Bob': 87, 'Charlie': 92}

We'll explore shallow vs deep copies in detail in Chapter 18. For now, remember: use copy() when you want an independent duplicate of a dictionary.

16.6.4) Merging Dictionaries with the | Operator (Python 3.9+)

Python 3.9 introduced the | operator for merging dictionaries. The | operator creates a new dictionary combining all keys from both dictionaries. For duplicate keys, the right-side value overrides the left-side value. Both original dictionaries remain unchanged.

python
defaults = {"theme": "light", "font_size": 12, "auto_save": True}
user_prefs = {"theme": "dark", "font_size": 14}
 
# Merge dictionaries (user_prefs overrides defaults)
config = defaults | user_prefs
print(config)  # Output: {'theme': 'dark', 'font_size': 14, 'auto_save': True}
 
# Original dictionaries unchanged
print(defaults)    # Output: {'theme': 'light', 'font_size': 12, 'auto_save': True}
print(user_prefs)  # Output: {'theme': 'dark', 'font_size': 14}

Here's another example showing how this is useful for combining data from multiple sources:

python
# Product information from two suppliers
supplier_a = {
    "laptop": {"price": 999.99, "stock": 15},
    "mouse": {"price": 29.99, "stock": 50}
}
 
supplier_b = {
    "laptop": {"price": 949.99, "stock": 20},  # Better price and stock
    "keyboard": {"price": 79.99, "stock": 30}
}
 
# Merge: supplier_b data overrides supplier_a for matching products
combined = supplier_a | supplier_b
print(combined)
# Output: {'laptop': {'price': 949.99, 'stock': 20}, 'mouse': {'price': 29.99, 'stock': 50}, 'keyboard': {'price': 79.99, 'stock': 30}}
 
# Now we have laptop from supplier_b (better deal), mouse from supplier_a, and keyboard from supplier_b

The |= operator merges in place (modifies the left dictionary):

python
config = {"theme": "light", "font_size": 12, "auto_save": True}
user_prefs = {"theme": "dark", "font_size": 14}
 
# Merge in place
config |= user_prefs
print(config)  # Output: {'theme': 'dark', 'font_size': 14, 'auto_save': True}

This is equivalent to using update() but more concise:

python
config = {"theme": "light", "font_size": 12}
user_prefs = {"theme": "dark", "font_size": 14}
 
# These are equivalent
config.update(user_prefs)
# config |= user_prefs
 
print(config)  # Output: {'theme': 'dark', 'font_size': 14}

16.7) Nested Dictionaries for Structured Data

16.7.1) Creating Nested Dictionaries

Why use nested dictionaries? Imagine tracking student information. You could create separate dictionaries: ages = {"Alice": 20, "Bob": 19}, majors = {"Alice": "CS", "Bob": "Math"}, gpas = {"Alice": 3.8, "Bob": 3.6}. But this becomes unwieldy—you have to keep three dictionaries synchronized, and looking up all information for one student requires three separate lookups. Nested dictionaries solve this by grouping related data together: one student name maps to all their information in a single lookup.

A nested dictionary is a dictionary that contains other dictionaries as values. This is useful for representing structured, hierarchical data:

python
# Student records with multiple attributes
students = {
    "Alice": {
        "age": 20,
        "major": "Computer Science",
        "gpa": 3.8
    },
    "Bob": {
        "age": 19,
        "major": "Mathematics",
        "gpa": 3.6
    },
    "Charlie": {
        "age": 21,
        "major": "Physics",
        "gpa": 3.9
    }
}
 
print(students)
# Output: {'Alice': {'age': 20, 'major': 'Computer Science', 'gpa': 3.8}, 'Bob': {'age': 19, 'major': 'Mathematics', 'gpa': 3.6}, 'Charlie': {'age': 21, 'major': 'Physics', 'gpa': 3.9}}

Each student name maps to another dictionary containing their attributes. This structure is much more flexible and maintainable than using separate dictionaries for each attribute.

students Dictionary

Alice

Bob

Charlie

age: 20

major: CS

gpa: 3.8

age: 19

major: Math

gpa: 3.6

age: 21

major: Physics

gpa: 3.9

16.7.2) Accessing Nested Values

To access nested values, use multiple square brackets:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
    "Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6}
}
 
# Access nested values
alice_age = students["Alice"]["age"]
print(alice_age)  # Output: 20
 
bob_major = students["Bob"]["major"]
print(bob_major)  # Output: Mathematics
 
# Use in expressions
print(f"Alice is {students['Alice']['age']} years old")
# Output: Alice is 20 years old
 
print(f"Bob's GPA: {students['Bob']['gpa']}")
# Output: Bob's GPA: 3.6

Each bracket accesses one level of nesting. First students["Alice"] gets the inner dictionary, then ["age"] gets the age value from that dictionary.

16.7.3) Safely Accessing Nested Values

When accessing nested dictionaries, you need to check that each level exists:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science"},
    "Bob": {"age": 19}  # Bob hasn't declared a major
}
 
# Unsafe access - might raise KeyError
# print(students["Bob"]["major"])  # PROBLEM: KeyError: 'major'
 
# Safe access with multiple checks
if "Bob" in students:
    if "major" in students["Bob"]:
        print(f"Bob's major: {students['Bob']['major']}")
    else:
        print("Bob hasn't declared a major")  # Output: Bob hasn't declared a major
 
# Using get() for safe nested access
bob_major = students.get("Bob", {}).get("major", "Undeclared")
print(f"Bob's major: {bob_major}")  # Output: Bob's major: Undeclared

The get() chain works because if "Bob" doesn't exist, get() returns an empty dictionary {}, and then the second get() safely returns "Undeclared".

16.7.4) Modifying Nested Dictionaries

You can add, update, or remove entries in nested dictionaries:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
    "Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6}
}
 
# Update a nested value
students["Alice"]["gpa"] = 3.9
print(f"Alice's new GPA: {students['Alice']['gpa']}")  # Output: Alice's new GPA: 3.9
 
# Add a new attribute to an existing student
students["Bob"]["email"] = "bob@university.edu"
print(students["Bob"])
# Output: {'age': 19, 'major': 'Mathematics', 'gpa': 3.6, 'email': 'bob@university.edu'}
 
# Add a new student with nested data
students["Charlie"] = {
    "age": 21,
    "major": "Physics",
    "gpa": 3.7
}
print(f"Number of students: {len(students)}")  # Output: Number of students: 3
 
# Remove an attribute
del students["Bob"]["email"]
print(students["Bob"])
# Output: {'age': 19, 'major': 'Mathematics', 'gpa': 3.6}

16.7.5) Iterating Over Nested Dictionaries

You can iterate over nested dictionaries using nested loops:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
    "Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6},
    "Charlie": {"age": 21, "major": "Physics", "gpa": 3.9}
}
 
# Iterate over students and their attributes
for name, info in students.items():
    print(f"\n{name}:")
    for key, value in info.items():
        print(f"  {key}: {value}")
 
# Output:
# Alice:
#   age: 20
#   major: Computer Science
#   gpa: 3.8
#
# Bob:
#   age: 19
#   major: Mathematics
#   gpa: 3.6
#
# Charlie:
#   age: 21
#   major: Physics
#   gpa: 3.9

Here's a practical example finding students who meet certain criteria:

python
students = {
    "Alice": {"age": 20, "major": "Computer Science", "gpa": 3.8},
    "Bob": {"age": 19, "major": "Mathematics", "gpa": 3.6},
    "Charlie": {"age": 21, "major": "Physics", "gpa": 3.9},
    "Diana": {"age": 20, "major": "Computer Science", "gpa": 3.5}
}
 
# Find CS students with GPA above 3.7
print("High-performing CS students:")
for name, info in students.items():
    if info["major"] == "Computer Science" and info["gpa"] >= 3.7:
        print(f"  {name} (GPA: {info['gpa']})")
 
# Output:
# High-performing CS students:
#   Alice (GPA: 3.8)

Dictionaries are one of Python's most powerful and versatile data structures. They provide fast lookups, flexible organization, and elegant solutions to common programming problems. As you continue learning Python, you'll find dictionaries appearing everywhere—from configuration settings to data processing to building complex applications.

The patterns we've covered in this chapter—counting, grouping, lookup tables, and data transformation—form the foundation for working with structured data in Python. In the next chapter, we'll explore sets, another collection type that complements dictionaries for working with unique, unordered data.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai