Python - Dictionary Tutorial (Complete Guide)

Dictionaries can be created using curly braces, the `dict()` constructor, or dictionary comprehensions. Each method serves different use cases.

Key Insights

  • Python dictionaries are mutable, unordered collections that store key-value pairs with O(1) average lookup time, making them ideal for fast data retrieval and mapping relationships
  • Dictionary comprehensions, the get() method with defaults, and setdefault() provide cleaner, more Pythonic alternatives to traditional iteration and conditional logic
  • Modern Python (3.7+) preserves insertion order in dictionaries, enabling predictable iteration while maintaining hash table performance characteristics

Creating and Initializing Dictionaries

Dictionaries can be created using curly braces, the dict() constructor, or dictionary comprehensions. Each method serves different use cases.

# Literal syntax
user = {'name': 'Alice', 'age': 30, 'role': 'admin'}

# dict() constructor
config = dict(host='localhost', port=5432, timeout=30)

# From list of tuples
pairs = [('a', 1), ('b', 2), ('c', 3)]
mapping = dict(pairs)

# Dictionary comprehension
squares = {x: x**2 for x in range(1, 6)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Empty dictionary
cache = {}
# or
cache = dict()

The fromkeys() method creates dictionaries with default values for multiple keys:

# Initialize with None
keys = ['cpu', 'memory', 'disk']
metrics = dict.fromkeys(keys)
# {'cpu': None, 'memory': None, 'disk': None}

# Initialize with specific value
counters = dict.fromkeys(['success', 'error', 'pending'], 0)
# {'success': 0, 'error': 0, 'pending': 0}

Accessing and Modifying Values

Direct key access raises KeyError if the key doesn’t exist. The get() method provides safer access with optional defaults.

user = {'name': 'Bob', 'age': 25}

# Direct access
print(user['name'])  # 'Bob'
# print(user['email'])  # KeyError

# Safe access with get()
email = user.get('email')  # None
email = user.get('email', 'no-email@example.com')  # default value

# Modify existing key
user['age'] = 26

# Add new key
user['email'] = 'bob@example.com'

# Update multiple values
user.update({'role': 'developer', 'active': True})

The setdefault() method returns a value if the key exists, otherwise inserts the key with a default value:

word_count = {}
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']

for word in words:
    word_count.setdefault(word, 0)
    word_count[word] += 1

# {'apple': 3, 'banana': 2, 'cherry': 1}

Dictionary Methods for Data Manipulation

The pop() and popitem() methods remove and return values, while clear() empties the dictionary.

settings = {'theme': 'dark', 'font_size': 14, 'auto_save': True}

# Remove specific key
theme = settings.pop('theme')  # 'dark'
# settings is now {'font_size': 14, 'auto_save': True}

# Pop with default (no KeyError)
language = settings.pop('language', 'en')  # 'en'

# Remove and return arbitrary key-value pair (LIFO in Python 3.7+)
item = settings.popitem()  # ('auto_save', True)

# Clear all items
settings.clear()  # {}

Iterating Over Dictionaries

Dictionaries provide three views: keys, values, and items. These views are dynamic and reflect changes to the dictionary.

inventory = {'apples': 50, 'bananas': 30, 'oranges': 45}

# Iterate over keys (default)
for item in inventory:
    print(item)

# Explicit keys iteration
for item in inventory.keys():
    print(f"{item}: {inventory[item]}")

# Iterate over values
for quantity in inventory.values():
    print(quantity)

# Iterate over key-value pairs
for item, quantity in inventory.items():
    print(f"{item}: {quantity}")
    
# Filter during iteration
low_stock = {k: v for k, v in inventory.items() if v < 40}
# {'bananas': 30}

Nested Dictionaries and Complex Structures

Dictionaries can contain other dictionaries, enabling hierarchical data structures.

users = {
    'user_001': {
        'name': 'Alice',
        'permissions': ['read', 'write'],
        'metadata': {'last_login': '2024-01-15', 'login_count': 42}
    },
    'user_002': {
        'name': 'Bob',
        'permissions': ['read'],
        'metadata': {'last_login': '2024-01-14', 'login_count': 18}
    }
}

# Access nested values
alice_permissions = users['user_001']['permissions']
bob_last_login = users['user_002']['metadata']['last_login']

# Safe nested access
def get_nested(d, *keys, default=None):
    for key in keys:
        if isinstance(d, dict):
            d = d.get(key, default)
        else:
            return default
    return d

login_count = get_nested(users, 'user_001', 'metadata', 'login_count')  # 42
invalid = get_nested(users, 'user_999', 'name', default='Unknown')  # 'Unknown'

Dictionary Comprehensions for Transformation

Dictionary comprehensions provide concise syntax for creating dictionaries from iterables with transformations and filters.

# Transform values
prices = {'apple': 0.5, 'banana': 0.3, 'orange': 0.6}
prices_cents = {item: price * 100 for item, price in prices.items()}
# {'apple': 50.0, 'banana': 30.0, 'orange': 60.0}

# Filter and transform
numbers = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
even_squares = {k: v**2 for k, v in numbers.items() if v % 2 == 0}
# {'b': 4, 'd': 16}

# Swap keys and values
original = {'a': 1, 'b': 2, 'c': 3}
swapped = {v: k for k, v in original.items()}
# {1: 'a', 2: 'b', 3: 'c'}

# Conditional values
scores = {'Alice': 85, 'Bob': 92, 'Charlie': 78}
grades = {name: 'Pass' if score >= 80 else 'Fail' 
          for name, score in scores.items()}
# {'Alice': 'Pass', 'Bob': 'Pass', 'Charlie': 'Fail'}

Merging and Combining Dictionaries

Python 3.9+ introduces the merge operator | and update operator |= for combining dictionaries.

defaults = {'timeout': 30, 'retries': 3, 'debug': False}
user_config = {'timeout': 60, 'debug': True}

# Merge with | operator (Python 3.9+)
config = defaults | user_config
# {'timeout': 60, 'retries': 3, 'debug': True}

# Update in place with |=
defaults |= user_config

# Pre-3.9: unpacking
config = {**defaults, **user_config}

# Update method (modifies in place)
defaults.update(user_config)

# Merge multiple dictionaries
db_config = {'host': 'localhost', 'port': 5432}
auth_config = {'user': 'admin', 'password': 'secret'}
full_config = defaults | db_config | auth_config

defaultdict for Automatic Default Values

The collections.defaultdict eliminates the need for existence checks when building dictionaries.

from collections import defaultdict

# Group items by category
items = [
    ('fruit', 'apple'),
    ('vegetable', 'carrot'),
    ('fruit', 'banana'),
    ('vegetable', 'spinach'),
    ('fruit', 'orange')
]

# Traditional approach
groups = {}
for category, item in items:
    if category not in groups:
        groups[category] = []
    groups[category].append(item)

# With defaultdict
groups = defaultdict(list)
for category, item in items:
    groups[category].append(item)
# defaultdict(<class 'list'>, {'fruit': ['apple', 'banana', 'orange'], 
#                               'vegetable': ['carrot', 'spinach']})

# Count occurrences
text = "the quick brown fox jumps over the lazy dog"
word_count = defaultdict(int)
for word in text.split():
    word_count[word] += 1

OrderedDict and Specialized Dictionary Types

While standard dictionaries maintain insertion order in Python 3.7+, OrderedDict provides additional methods and guarantees.

from collections import OrderedDict

# OrderedDict with move_to_end
cache = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
cache.move_to_end('a')  # Move to end
# OrderedDict([('b', 2), ('c', 3), ('a', 1)])

cache.move_to_end('b', last=False)  # Move to beginning
# OrderedDict([('b', 2), ('c', 3), ('a', 1)])

# Counter for frequency counting
from collections import Counter

words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
counter = Counter(words)
# Counter({'apple': 3, 'banana': 2, 'cherry': 1})

print(counter.most_common(2))  # [('apple', 3), ('banana', 2)]
counter.update(['apple', 'date'])  # Add more counts

Performance Considerations and Best Practices

Dictionary operations have specific performance characteristics that affect design decisions.

import sys

# Memory efficiency: dict vs list of tuples
data_dict = {i: i**2 for i in range(1000)}
data_list = [(i, i**2) for i in range(1000)]

print(sys.getsizeof(data_dict))  # ~36968 bytes
print(sys.getsizeof(data_list))  # ~9016 bytes (but O(n) lookup)

# Use membership testing efficiently
cache = {'key1': 'value1', 'key2': 'value2'}

# Fast: O(1)
if 'key1' in cache:
    print(cache['key1'])

# Slow: O(n)
if 'value1' in cache.values():
    print('found')

# Pre-compute reverse lookup if needed
reverse_cache = {v: k for k, v in cache.items()}

Dictionaries are fundamental to Python programming, appearing in JSON parsing, configuration management, caching, and data transformation. Understanding their methods, performance characteristics, and specialized variants enables you to write efficient, idiomatic Python code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.