Python Dictionaries: Complete Guide with Examples
Python dictionaries store data as key-value pairs, providing fast lookups regardless of dictionary size. Unlike lists that use integer indices, dictionaries use hashable keys—typically strings,...
Key Insights
- Dictionaries are Python’s most versatile data structure, offering O(1) average-case lookup performance and mutable key-value storage that outperforms lists for data retrieval by identifier.
- Modern Python (3.9+) provides powerful dictionary operations like the merge operator (|) and union assignment (|=), making dictionary manipulation cleaner than traditional .update() patterns.
- The biggest pitfalls with dictionaries stem from mutability—shallow copies, mutable default arguments, and reference sharing cause subtle bugs that proper defensive copying and tools like defaultdict prevent.
Dictionary Basics and Creation
Python dictionaries store data as key-value pairs, providing fast lookups regardless of dictionary size. Unlike lists that use integer indices, dictionaries use hashable keys—typically strings, numbers, or tuples—to access values.
Dictionaries are mutable (you can change them after creation) and maintain insertion order as of Python 3.7+. This ordering guarantee makes dictionaries predictable for iteration while maintaining their performance characteristics.
Here are the main ways to create dictionaries:
# Literal syntax - most common
user = {'name': 'Alice', 'age': 30, 'role': 'engineer'}
# dict() constructor with keyword arguments
user = dict(name='Alice', age=30, role='engineer')
# dict() from tuples
pairs = [('name', 'Alice'), ('age', 30), ('role', 'engineer')]
user = dict(pairs)
# Create dictionary with default values
keys = ['name', 'age', 'role']
empty_user = dict.fromkeys(keys) # All values are None
default_user = dict.fromkeys(keys, 'unknown') # All values are 'unknown'
# Dictionary comprehension
numbers = {x: x**2 for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
The literal syntax is typically clearest. Use dict() constructor when keys come from variables or when converting from other structures.
Accessing and Modifying Dictionary Data
Access dictionary values using bracket notation or the safer .get() method:
user = {'name': 'Alice', 'age': 30}
# Bracket notation - raises KeyError if key doesn't exist
name = user['name'] # 'Alice'
# .get() method - returns None or specified default if key doesn't exist
age = user.get('age') # 30
role = user.get('role') # None
role = user.get('role', 'unknown') # 'unknown'
Always prefer .get() when a key might not exist. It prevents exceptions and makes your code more defensive.
Modifying dictionaries is straightforward:
user = {'name': 'Alice', 'age': 30}
# Add or update key-value pairs
user['role'] = 'engineer' # Add new key
user['age'] = 31 # Update existing key
# Update multiple values at once
user.update({'age': 32, 'department': 'engineering'})
user.update(age=33, location='NYC') # Alternative syntax
# Delete entries
del user['department'] # Raises KeyError if key doesn't exist
# .pop() removes and returns value
age = user.pop('age') # Returns 33, removes 'age' key
role = user.pop('role', 'unknown') # Safe with default value
# .popitem() removes and returns last inserted (key, value) pair
last_item = user.popitem()
# Clear all entries
user.clear()
The .pop() method is particularly useful because it atomically retrieves and removes a value, preventing race conditions in certain scenarios.
Dictionary Methods and Operations
Understanding dictionary methods unlocks efficient data manipulation:
user = {'name': 'Alice', 'age': 30, 'role': 'engineer'}
# Get views of keys, values, and items
keys = user.keys() # dict_keys(['name', 'age', 'role'])
values = user.values() # dict_values(['Alice', 30, 'engineer'])
items = user.items() # dict_items([('name', 'Alice'), ('age', 30), ('role', 'engineer')])
# These views are dynamic - they reflect dictionary changes
print(list(keys)) # ['name', 'age', 'role']
user['location'] = 'NYC'
print(list(keys)) # ['name', 'age', 'role', 'location']
# Membership testing - checks keys only
if 'name' in user:
print(f"User name: {user['name']}")
# Check if key doesn't exist
if 'salary' not in user:
user['salary'] = 75000
# Iterate over keys (default)
for key in user:
print(f"{key}: {user[key]}")
# Iterate over key-value pairs (preferred)
for key, value in user.items():
print(f"{key}: {value}")
# Iterate over values only
for value in user.values():
print(value)
# .setdefault() - get value or set if doesn't exist
role = user.setdefault('role', 'unknown') # Returns 'engineer'
dept = user.setdefault('department', 'engineering') # Sets and returns 'engineering'
The .setdefault() method is underutilized but powerful—it atomically checks for a key and sets a default if missing, returning the value either way.
Nested Dictionaries and Complex Structures
Real-world data often requires nested dictionaries:
# JSON-like structure
company = {
'name': 'TechCorp',
'employees': [
{'name': 'Alice', 'role': 'engineer', 'skills': ['Python', 'Go']},
{'name': 'Bob', 'role': 'designer', 'skills': ['Figma', 'CSS']}
],
'locations': {
'headquarters': {'city': 'NYC', 'employees': 50},
'branch': {'city': 'SF', 'employees': 30}
}
}
# Access nested data
hq_city = company['locations']['headquarters']['city'] # 'NYC'
first_employee_name = company['employees'][0]['name'] # 'Alice'
# Safe nested access with .get()
branch_employees = company.get('locations', {}).get('branch', {}).get('employees', 0)
# Modify nested structures
company['locations']['headquarters']['employees'] = 55
company['employees'][0]['skills'].append('Rust')
Copying nested dictionaries requires care:
import copy
original = {'a': 1, 'b': {'c': 2}}
# Shallow copy - nested objects are references
shallow = original.copy()
shallow['b']['c'] = 99
print(original['b']['c']) # 99 - original changed!
# Deep copy - complete independent copy
deep = copy.deepcopy(original)
deep['b']['c'] = 100
print(original['b']['c']) # 99 - original unchanged
Use copy.deepcopy() when you need truly independent copies of nested structures.
Dictionary Comprehensions and Advanced Techniques
Dictionary comprehensions create dictionaries concisely:
# Basic comprehension
squares = {x: x**2 for x in range(6)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# With conditional filtering
even_squares = {x: x**2 for x in range(10) if x % 2 == 0}
# Transform existing dictionary
prices = {'apple': 0.5, 'banana': 0.3, 'orange': 0.6}
prices_cents = {item: int(price * 100) for item, price in prices.items()}
# Swap keys and values
inverted = {value: key for key, value in prices.items()}
# Filter dictionary
expensive = {k: v for k, v in prices.items() if v > 0.4}
Python 3.9+ introduced merge operators that simplify dictionary combining:
defaults = {'theme': 'dark', 'language': 'en', 'notifications': True}
user_prefs = {'theme': 'light', 'language': 'es'}
# Merge with | operator (3.9+)
settings = defaults | user_prefs # user_prefs overrides defaults
# {'theme': 'light', 'language': 'es', 'notifications': True}
# In-place merge with |= operator
defaults |= user_prefs # Modifies defaults
# Pre-3.9 approach
settings = {**defaults, **user_prefs} # Unpacking syntax
The merge operator is clearer than .update() because it returns a new dictionary without mutating the originals.
Common Use Cases and Best Practices
Dictionaries excel at counting, grouping, and caching:
# Counting occurrences
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
# {'apple': 3, 'banana': 2, 'cherry': 1}
# Grouping data
users = [
{'name': 'Alice', 'role': 'engineer'},
{'name': 'Bob', 'role': 'designer'},
{'name': 'Charlie', 'role': 'engineer'}
]
by_role = {}
for user in users:
role = user['role']
by_role.setdefault(role, []).append(user['name'])
# {'engineer': ['Alice', 'Charlie'], 'designer': ['Bob']}
# Memoization pattern
cache = {}
def expensive_computation(n):
if n in cache:
return cache[n]
result = n ** 2 # Simulate expensive operation
cache[n] = result
return result
Use dictionaries when you need:
- Fast lookups by identifier (O(1) average case)
- Key-value associations
- Counting or grouping
- Configuration or settings storage
Use lists when you need:
- Ordered sequences accessed by position
- Duplicate values
- Stack or queue operations
Common Pitfalls and How to Avoid Them
The most dangerous dictionary pitfall is mutable default arguments:
# WRONG - mutable default argument
def add_item(item, inventory={}):
inventory[item] = inventory.get(item, 0) + 1
return inventory
add_item('apple') # {'apple': 1}
add_item('banana') # {'apple': 1, 'banana': 1} - Unexpected!
# CORRECT - use None as default
def add_item(item, inventory=None):
if inventory is None:
inventory = {}
inventory[item] = inventory.get(item, 0) + 1
return inventory
Handle missing keys gracefully:
from collections import defaultdict
# Option 1: Use .get() with default
config = {'timeout': 30}
retries = config.get('retries', 3)
# Option 2: Use try/except for KeyError
try:
value = config['missing_key']
except KeyError:
value = 'default'
# Option 3: Use defaultdict for automatic defaults
counts = defaultdict(int) # Missing keys default to 0
counts['apple'] += 1 # No KeyError
grouped = defaultdict(list) # Missing keys default to []
grouped['fruits'].append('apple') # No KeyError
The defaultdict from the collections module eliminates most KeyError scenarios and makes counting/grouping code cleaner.
Remember that dictionary keys must be hashable—immutable types like strings, numbers, and tuples work, but lists and dictionaries don’t:
# Valid keys
valid = {
'string': 1,
42: 2,
(1, 2): 3,
True: 4 # Note: True and 1 are the same key!
}
# Invalid keys
invalid = {
[1, 2]: 'list', # TypeError: unhashable type: 'list'
{'a': 1}: 'dict' # TypeError: unhashable type: 'dict'
}
Dictionaries are Python’s workhorse data structure. Master them, understand their performance characteristics, and use defensive programming patterns like .get() and defaultdict to write robust code that handles edge cases gracefully.