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:
# 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:
# 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:
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: 87This 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:
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:
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 bookThe 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:
# 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 Math16.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
# 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:
# 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: eastValues, on the other hand, can be any type—mutable or immutable:
# 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: 1We'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:
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:
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:
# 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:
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():
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:
# 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:
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:
# 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: en16.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:
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:
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:
grades = {"Alice": 95, "Bob": 87}
# Safe deletion
if "Charlie" in grades:
del grades["Charlie"]
else:
print("Charlie not found") # Output: Charlie not found16.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:
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:
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:
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: 0This is more explicit than reassigning to an empty dictionary (grades = {}), especially when other variables reference the same dictionary:
# 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 keysvalues(): Returns a view of all valuesitems(): Returns a view of all key-value pairs (as tuples)
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:
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:
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:
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)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:
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: 3You can also check membership directly on the dictionary without calling keys():
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:
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: 8716.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:
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: 95The items() view is particularly useful for iteration, which we'll cover in detail in the next section. Here's a preview:
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: 9216.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:
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# Iterate over keys (implicit)
for name in grades:
print(name)
# Output:
# Alice
# Bob
# CharlieThis is equivalent to iterating over grades.keys():
grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
# Iterate over keys (explicit)
for name in grades.keys():
print(name)
# Output:
# Alice
# Bob
# CharlieBoth approaches work identically. The implicit version (without .keys()) is more common and concise.
Here's a practical example using keys to access values:
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():
grades = {"Alice": 95, "Bob": 87, "Charlie": 92, "Diana": 88}
# Iterate over values
for grade in grades.values():
print(grade)
# Output:
# 95
# 87
# 92
# 88This is useful when you need to process values but don't care which key they're associated with:
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:
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 92Notice 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:
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 9216.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:
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 iterationIn 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:
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:
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:
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 bookThis 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:
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: 9516.6.2) Getting the Number of Entries with len()
The len() function returns the number of key-value pairs in a dictionary:
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: 3This is useful for checking if a dictionary is empty or for reporting statistics:
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.516.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:
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:
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.
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:
# 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_bThe |= operator merges in place (modifies the left dictionary):
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:
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:
# 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.
16.7.2) Accessing Nested Values
To access nested values, use multiple square brackets:
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.6Each 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:
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: UndeclaredThe 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:
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:
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.9Here's a practical example finding students who meet certain criteria:
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.