Python Dictionaries: Mastering Keys, Values, and Iteration
Introduction
Python dictionaries are one of the most powerful and frequently used data structures in the language. Unlike lists that use numeric indices, dictionaries use keys to access values, making them ideal for storing and retrieving data in a meaningful, organized way.
Think of a dictionary like a real-world phone book: instead of looking up a person by their position in a list, you look them up by their name (the key) to find their phone number (the value). This key-value relationship makes dictionaries incredibly useful for real-world programming tasks.
In this guide, we’ll explore the fundamentals of Python dictionaries, focusing on three core concepts: keys, values, and iteration techniques. By the end, you’ll understand how to work with dictionaries effectively and choose the right iteration method for your needs.
What Are Dictionaries?
A dictionary is an unordered (in Python 3.7+, insertion-ordered), mutable collection of key-value pairs. Each key maps to a corresponding value, allowing you to store and retrieve data efficiently.
Key characteristics:
- Key-value pairs: Data is stored as associations between keys and values
- Mutable: You can add, remove, or modify key-value pairs after creation
- Unordered (insertion-ordered in 3.7+): While modern Python maintains insertion order, dictionaries are conceptually unordered
- Indexed by keys: Access values using keys instead of numeric indices
- Flexible: Keys and values can be of various types (with restrictions on keys)
Creating Dictionaries
# Empty dictionary
empty_dict = {}
# Dictionary literal with key-value pairs
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
# Using the dict() constructor
config = dict(host="localhost", port=8080, debug=True)
# Dictionary comprehension (covered later)
squares = {x: x**2 for x in range(5)}
print(squares) # Output: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
Part 1: Understanding Dictionary Keys
What Makes a Valid Key?
Not every Python object can be a dictionary key. Keys must satisfy two critical requirements:
1. Immutability
Keys must be immutableโthey cannot change after creation. This is because Python uses a hashing mechanism to quickly locate values. If a key could change, the dictionary would lose track of the value.
# Valid: immutable types as keys
valid_dict = {
"name": "Alice", # string (immutable)
42: "answer", # integer (immutable)
3.14: "pi", # float (immutable)
True: "boolean", # boolean (immutable)
(1, 2): "coordinates" # tuple (immutable)
}
# Invalid: mutable types as keys
# invalid_dict = {[1, 2]: "list"} # TypeError: unhashable type: 'list'
# invalid_dict = {{}: "dict"} # TypeError: unhashable type: 'dict'
2. Hashability
Keys must be hashable, meaning they can be converted to a hash valueโa unique integer that Python uses for fast lookups. All immutable built-in types are hashable, but not all immutable objects are (for example, custom objects require special implementation).
# Hashable types (can be keys)
hashable_keys = {
"string": 1,
123: 2,
3.14: 3,
(1, 2, 3): 4,
frozenset([1, 2]): 5,
None: 6
}
# Non-hashable types (cannot be keys)
# unhashable_dict = {[1, 2, 3]: "list"} # TypeError
# unhashable_dict = {{1: 2}: "dict"} # TypeError
# unhashable_dict = {set([1, 2]): "set"} # TypeError
Types of Keys
Strings as Keys
Strings are the most common key type. They’re readable and meaningful:
user_profile = {
"username": "alice_wonder",
"email": "[email protected]",
"verified": True,
"join_date": "2023-01-15"
}
print(user_profile["username"]) # Output: alice_wonder
Integers as Keys
Integers work well for numeric indices or IDs:
student_grades = {
101: 85,
102: 92,
103: 78,
104: 95
}
print(student_grades[102]) # Output: 92
Tuples as Keys
Tuples are useful for composite keys, like coordinates:
# Map coordinates to locations
locations = {
(0, 0): "origin",
(1, 0): "east",
(0, 1): "north",
(-1, 0): "west",
(0, -1): "south"
}
print(locations[(1, 0)]) # Output: east
Mixed Key Types
A single dictionary can have different key types:
mixed_keys = {
"name": "Alice",
42: "the answer",
(1, 2): "coordinates",
3.14: "pi"
}
print(mixed_keys["name"]) # Output: Alice
print(mixed_keys[42]) # Output: the answer
print(mixed_keys[(1, 2)]) # Output: coordinates
Key Best Practices
1. Use meaningful keys
# Good: descriptive keys
person = {
"first_name": "Alice",
"last_name": "Smith",
"email": "[email protected]"
}
# Avoid: cryptic keys
person_bad = {
"fn": "Alice",
"ln": "Smith",
"em": "[email protected]"
}
2. Use consistent naming conventions
# Good: consistent snake_case
config = {
"database_host": "localhost",
"database_port": 5432,
"max_connections": 100
}
# Avoid: inconsistent naming
config_bad = {
"DatabaseHost": "localhost",
"database_port": 5432,
"MAX_CONNECTIONS": 100
}
3. Avoid mutable keys
# Good: immutable key
cache = {
(2023, 12, 25): "Christmas"
}
# Avoid: mutable key (will cause error)
# cache = {
# [2023, 12, 25]: "Christmas" # TypeError
# }
4. Use lowercase strings for keys
# Good: lowercase keys
settings = {
"theme": "dark",
"language": "en",
"notifications": True
}
# Avoid: uppercase keys (less conventional)
settings_bad = {
"THEME": "dark",
"LANGUAGE": "en",
"NOTIFICATIONS": True
}
Part 2: Understanding Dictionary Values
Value Flexibility
Unlike keys, dictionary values have no restrictions. They can be any Python objectโmutable or immutable, simple or complex.
# Values of different types
flexible_dict = {
"string_value": "hello",
"integer_value": 42,
"float_value": 3.14,
"boolean_value": True,
"none_value": None,
"list_value": [1, 2, 3],
"tuple_value": (4, 5, 6),
"dict_value": {"nested": "dictionary"},
"set_value": {7, 8, 9}
}
Accessing Values
Direct Access
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
print(person["name"]) # Output: Alice
print(person["age"]) # Output: 30
Using .get() Method
The .get() method is safer than direct access because it returns None (or a default value) instead of raising a KeyError if the key doesn’t exist:
person = {
"name": "Alice",
"age": 30
}
# Direct access (raises KeyError if key missing)
# print(person["email"]) # KeyError: 'email'
# Using .get() (returns None if key missing)
print(person.get("email")) # Output: None
print(person.get("email", "N/A")) # Output: N/A
print(person.get("name")) # Output: Alice
Checking Key Existence
person = {
"name": "Alice",
"age": 30
}
# Using 'in' operator
if "name" in person:
print(f"Name: {person['name']}") # Output: Name: Alice
if "email" not in person:
print("Email not provided") # Output: Email not provided
Modifying Values
person = {
"name": "Alice",
"age": 30
}
# Update existing value
person["age"] = 31
print(person) # Output: {'name': 'Alice', 'age': 31}
# Add new key-value pair
person["email"] = "[email protected]"
print(person) # Output: {'name': 'Alice', 'age': 31, 'email': '[email protected]'}
# Using .update() to add multiple pairs
person.update({
"city": "New York",
"country": "USA"
})
print(person)
# Output: {'name': 'Alice', 'age': 31, 'email': '[email protected]', 'city': 'New York', 'country': 'USA'}
Nested Dictionaries
Dictionaries can contain other dictionaries as values, creating nested structures:
company = {
"name": "TechCorp",
"employees": {
"alice": {
"position": "Engineer",
"salary": 100000
},
"bob": {
"position": "Manager",
"salary": 120000
}
},
"locations": {
"headquarters": "New York",
"branch": "San Francisco"
}
}
# Access nested values
print(company["employees"]["alice"]["position"]) # Output: Engineer
print(company["locations"]["headquarters"]) # Output: New York
Common Value Operations
Appending to List Values
# Dictionary with list values
inventory = {
"apples": [1, 2, 3],
"oranges": [4, 5]
}
# Append to a list value
inventory["apples"].append(4)
print(inventory) # Output: {'apples': [1, 2, 3, 4], 'oranges': [4, 5]}
Incrementing Numeric Values
# Dictionary with numeric values
scores = {
"alice": 100,
"bob": 85,
"charlie": 92
}
# Increment a value
scores["alice"] += 5
print(scores) # Output: {'alice': 105, 'bob': 85, 'charlie': 92}
Part 3: Iteration Techniques
Iterating Over Keys
Using .keys() Method
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
# Explicit .keys() method
for key in person.keys():
print(key)
# Output:
# name
# age
# city
Direct Iteration (Implicit Keys)
When you iterate over a dictionary directly, you iterate over its keys:
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
# Direct iteration (same as .keys())
for key in person:
print(key)
# Output:
# name
# age
# city
When to use: When you only need the keys and don’t need the values.
Iterating Over Values
Using .values() Method
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
for value in person.values():
print(value)
# Output:
# Alice
# 30
# New York
When to use: When you only need the values and don’t need the keys.
Iterating Over Key-Value Pairs
Using .items() Method
The .items() method returns tuples of (key, value) pairs. This is the most common iteration pattern:
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
for key, value in person.items():
print(f"{key}: {value}")
# Output:
# name: Alice
# age: 30
# city: New York
When to use: When you need both keys and values. This is the most Pythonic approach.
Unpacking Without Destructuring
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
# Get the pair without unpacking
for pair in person.items():
print(pair)
# Output:
# ('name', 'Alice')
# ('age', 30)
# ('city', 'New York')
Dictionary Comprehensions
Dictionary comprehensions provide a concise way to create new dictionaries:
Basic Dictionary Comprehension
# Create a dictionary of squares
squares = {x: x**2 for x in range(5)}
print(squares) # Output: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# Create a dictionary from a list
fruits = ["apple", "banana", "cherry"]
fruit_lengths = {fruit: len(fruit) for fruit in fruits}
print(fruit_lengths) # Output: {'apple': 5, 'banana': 6, 'cherry': 6}
Dictionary Comprehension with Conditions
# Filter and transform
numbers = range(10)
even_squares = {x: x**2 for x in numbers if x % 2 == 0}
print(even_squares) # Output: {0: 0, 2: 4, 4: 16, 6: 36, 8: 64}
# Conditional values
scores = {"alice": 85, "bob": 92, "charlie": 78}
grades = {name: "pass" if score >= 80 else "fail" for name, score in scores.items()}
print(grades) # Output: {'alice': 'pass', 'bob': 'pass', 'charlie': 'fail'}
Transforming Existing Dictionaries
# Convert keys to uppercase
original = {"name": "alice", "city": "new york"}
uppercase = {key.upper(): value for key, value in original.items()}
print(uppercase) # Output: {'NAME': 'alice', 'CITY': 'new york'}
# Convert values to uppercase
uppercase_values = {key: value.upper() for key, value in original.items()}
print(uppercase_values) # Output: {'name': 'ALICE', 'city': 'NEW YORK'}
Nested Dictionary Iteration
Iterating Over Nested Dictionaries
company = {
"engineering": {
"alice": 100000,
"bob": 95000
},
"sales": {
"charlie": 80000,
"diana": 85000
}
}
# Iterate over departments and employees
for department, employees in company.items():
print(f"Department: {department}")
for employee, salary in employees.items():
print(f" {employee}: ${salary}")
# Output:
# Department: engineering
# alice: $100000
# bob: $95000
# Department: sales
# charlie: $80000
# diana: $85000
Flattening Nested Dictionaries
# Convert nested dictionary to flat key-value pairs
nested = {
"user": {
"name": "Alice",
"email": "[email protected]"
},
"settings": {
"theme": "dark",
"notifications": True
}
}
# Flatten using nested loops
flattened = {}
for category, data in nested.items():
for key, value in data.items():
flattened[f"{category}_{key}"] = value
print(flattened)
# Output: {'user_name': 'Alice', 'user_email': '[email protected]', 'settings_theme': 'dark', 'settings_notifications': True}
Nested Dictionary Comprehension
# Create nested dictionary using comprehension
matrix = {
i: {j: i*j for j in range(1, 4)}
for i in range(1, 4)
}
print(matrix)
# Output: {1: {1: 1, 2: 2, 3: 3}, 2: {1: 2, 2: 4, 3: 6}, 3: {1: 3, 2: 6, 3: 9}}
# Iterate over nested comprehension
for i, row in matrix.items():
for j, value in row.items():
print(f"matrix[{i}][{j}] = {value}")
Advanced Iteration Patterns
Using enumerate() with .items()
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
for index, (key, value) in enumerate(person.items(), 1):
print(f"{index}. {key}: {value}")
# Output:
# 1. name: Alice
# 2. age: 30
# 3. city: New York
Sorting During Iteration
scores = {
"alice": 92,
"bob": 85,
"charlie": 95,
"diana": 88
}
# Sort by keys
print("Sorted by keys:")
for name in sorted(scores.keys()):
print(f"{name}: {scores[name]}")
# Sort by values
print("\nSorted by values:")
for name, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
print(f"{name}: {score}")
# Output:
# Sorted by keys:
# alice: 92
# bob: 85
# charlie: 95
# diana: 88
#
# Sorted by values:
# charlie: 95
# alice: 92
# diana: 88
# bob: 85
Filtering During Iteration
scores = {
"alice": 92,
"bob": 85,
"charlie": 95,
"diana": 88
}
# Iterate only over passing scores (>= 90)
print("Passing scores:")
for name, score in scores.items():
if score >= 90:
print(f"{name}: {score}")
# Output:
# alice: 92
# charlie: 95
Practical Use Cases
Use Case 1: Counting Occurrences
# Count word frequencies
text = "the quick brown fox jumps over the lazy dog"
words = text.split()
word_count = {}
for word in words:
word_count[word] = word_count.get(word, 0) + 1
print(word_count)
# Output: {'the': 2, 'quick': 1, 'brown': 1, 'fox': 1, 'jumps': 1, 'over': 1, 'lazy': 1, 'dog': 1}
# More Pythonic using defaultdict
from collections import defaultdict
word_count = defaultdict(int)
for word in words:
word_count[word] += 1
print(dict(word_count))
Use Case 2: Grouping Data
# Group students by grade
students = [
{"name": "Alice", "grade": "A"},
{"name": "Bob", "grade": "B"},
{"name": "Charlie", "grade": "A"},
{"name": "Diana", "grade": "B"},
{"name": "Eve", "grade": "A"}
]
# Group by grade
grades = {}
for student in students:
grade = student["grade"]
if grade not in grades:
grades[grade] = []
grades[grade].append(student["name"])
print(grades)
# Output: {'A': ['Alice', 'Charlie', 'Eve'], 'B': ['Bob', 'Diana']}
# Using dictionary comprehension
grades_comp = {
grade: [s["name"] for s in students if s["grade"] == grade]
for grade in set(s["grade"] for s in students)
}
print(grades_comp)
Use Case 3: Caching Results
# Cache expensive calculations
cache = {}
def fibonacci(n):
if n in cache:
return cache[n]
if n <= 1:
result = n
else:
result = fibonacci(n-1) + fibonacci(n-2)
cache[n] = result
return result
print(fibonacci(10)) # Output: 55
print(cache) # Shows all cached values
Use Case 4: Configuration Management
# Application configuration
config = {
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp"
},
"server": {
"host": "0.0.0.0",
"port": 8000,
"debug": True
},
"logging": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
}
# Access configuration
db_host = config["database"]["host"]
server_port = config["server"]["port"]
# Iterate over all settings
for section, settings in config.items():
print(f"[{section}]")
for key, value in settings.items():
print(f" {key} = {value}")
Common Pitfalls and How to Avoid Them
Pitfall 1: KeyError When Accessing Missing Keys
person = {"name": "Alice"}
# Bad: raises KeyError
# print(person["age"]) # KeyError: 'age'
# Good: use .get() with default
print(person.get("age", "Unknown")) # Output: Unknown
# Good: check if key exists first
if "age" in person:
print(person["age"])
else:
print("Age not provided")
Pitfall 2: Modifying Dictionary While Iterating
# Bad: modifying dictionary while iterating
scores = {"alice": 90, "bob": 85, "charlie": 92}
# for name in scores:
# if scores[name] < 90:
# del scores[name] # RuntimeError: dictionary changed size during iteration
# Good: iterate over a copy of keys
scores = {"alice": 90, "bob": 85, "charlie": 92}
for name in list(scores.keys()):
if scores[name] < 90:
del scores[name]
print(scores) # Output: {'alice': 90, 'charlie': 92}
# Better: use dictionary comprehension
scores = {"alice": 90, "bob": 85, "charlie": 92}
scores = {name: score for name, score in scores.items() if score >= 90}
print(scores) # Output: {'alice': 90, 'charlie': 92}
Pitfall 3: Using Mutable Objects as Keys
# Bad: using list as key
# my_dict = {[1, 2]: "value"} # TypeError: unhashable type: 'list'
# Good: use tuple instead
my_dict = {(1, 2): "value"}
print(my_dict[(1, 2)]) # Output: value
Pitfall 4: Shallow Copy Issues with Nested Dictionaries
# Bad: shallow copy doesn't copy nested dictionaries
original = {
"user": {"name": "Alice", "age": 30}
}
shallow_copy = original.copy()
shallow_copy["user"]["age"] = 31
print(original) # Output: {'user': {'name': 'Alice', 'age': 31}} - MODIFIED!
# Good: use deep copy for nested dictionaries
import copy
original = {
"user": {"name": "Alice", "age": 30}
}
deep_copy = copy.deepcopy(original)
deep_copy["user"]["age"] = 31
print(original) # Output: {'user': {'name': 'Alice', 'age': 30}} - UNCHANGED
Pitfall 5: Assuming Dictionary Order (Pre-3.7)
# In Python 3.7+, dictionaries maintain insertion order
# This is guaranteed behavior, not just an implementation detail
my_dict = {"z": 1, "a": 2, "m": 3}
print(list(my_dict.keys())) # Output: ['z', 'a', 'm'] - insertion order preserved
# If you need sorted order, sort explicitly
print(sorted(my_dict.keys())) # Output: ['a', 'm', 'z']
Dictionary Methods Reference
| Method | Purpose | Example |
|---|---|---|
.keys() |
Get all keys | dict.keys() |
.values() |
Get all values | dict.values() |
.items() |
Get key-value pairs | dict.items() |
.get(key, default) |
Get value safely | dict.get("key", "default") |
.pop(key, default) |
Remove and return value | dict.pop("key") |
.update(other) |
Merge dictionaries | dict.update(other_dict) |
.clear() |
Remove all items | dict.clear() |
.copy() |
Create shallow copy | new_dict = dict.copy() |
.setdefault(key, default) |
Get or set default | dict.setdefault("key", "default") |
.fromkeys(keys, value) |
Create dict from keys | dict.fromkeys(["a", "b"], 0) |
Best Practices
1. Use meaningful, descriptive keys
# Good
user = {
"first_name": "Alice",
"last_name": "Smith",
"email_address": "[email protected]"
}
# Avoid
user = {
"fn": "Alice",
"ln": "Smith",
"em": "[email protected]"
}
2. Use .get() for safe access
# Good
age = person.get("age", 0)
# Avoid
# age = person["age"] # May raise KeyError
3. Use .items() when you need both keys and values
# Good
for key, value in my_dict.items():
print(f"{key}: {value}")
# Less efficient
for key in my_dict:
print(f"{key}: {my_dict[key]}")
4. Use dictionary comprehensions for transformations
# Good
squared = {x: x**2 for x in range(5)}
# Less Pythonic
squared = {}
for x in range(5):
squared[x] = x**2
5. Use defaultdict for counting or grouping
from collections import defaultdict
# Good
word_count = defaultdict(int)
for word in words:
word_count[word] += 1
# More verbose
word_count = {}
for word in words:
word_count[word] = word_count.get(word, 0) + 1
Conclusion
Python dictionaries are incredibly versatile data structures that form the foundation of many Python programs. Understanding keys, values, and iteration techniques is essential for writing efficient, readable code.
Key takeaways:
- Keys must be immutable and hashable; use meaningful, descriptive names
- Values can be any Python object, including other dictionaries
- Iteration methods (
.keys(),.values(),.items()) serve different purposesโuse.items()when you need both - Dictionary comprehensions provide elegant, Pythonic ways to create and transform dictionaries
- Nested dictionaries enable complex data structures for real-world applications
- Safe access with
.get()preventsKeyErrorexceptions - Avoid modifying dictionaries while iterating over them
Master these concepts, and you’ll be able to work with dictionaries confidently in any Python project. Whether you’re building web applications, data analysis tools, or system scripts, dictionaries will be your go-to data structure for organizing and accessing data efficiently.
Comments