Skip to content

Python Dictionaries

A dictionary (dict) Mutable stores key-value pairs. You look up a value by its key rather than by a numeric position, which makes dictionaries ideal for records, lookups, counters, and configuration. Since Python 3.7, dictionaries also remember insertion order.

TaskSyntaxExample
Create{key: value}, dict(...){'name': 'Ann', 'age': 31}
Readd[key], d.get(key, default)d.get('city', 'Unknown')
Add or changed[key] = valued['age'] = 32
Merged.update(other)d.update({'age': 33})
Removedel d[key], d.pop(key, default)d.pop('age', None)
Check a keykey in d'name' in d
Sizelen(d)len({'a': 1}) gives 1
Loop.keys(), .values(), .items()for k, v in d.items():
Build{k: v for ...}{n: n ** 2 for n in range(3)}

creating_dictionaries.py
pizza = {
'name': 'Margherita Pizza',
'price': 8.9,
'calories_per_slice': 250,
'toppings': ['mozzarella', 'basil'], # Values can be any type, even a list
}
empty = {} # An empty dict, not an empty set (use set() for that)
from_pairs = dict([('a', 1), ('b', 2)]) # {'a': 1, 'b': 2}
from_kwargs = dict(name='Ann', age=31) # {'name': 'Ann', 'age': 31}
from_zip = dict(zip(['x', 'y'], [1, 2])) # {'x': 1, 'y': 2}
defaults = dict.fromkeys(['a', 'b'], 0) # {'a': 0, 'b': 0}

The literal syntax {...} is the idiomatic choice. It is also slightly faster than dict(), because it avoids a function lookup.


A key must be hashable, which in practice means immutable: strings, numbers, booleans, None, tuples (containing only hashable items), and frozensets. Lists, dictionaries, and sets cannot be keys.

valid_and_invalid_keys.py
ok = {'name': 1, 42: 'int key', (1, 2): 'tuple key'}
bad = {[1, 2]: 'x'} # TypeError: unhashable type: 'list'
bad = {(1, [2]): 'x'} # TypeError: a tuple holding a list is unhashable too

Assigning to an existing key overwrites its value. Keys that compare equal are treated as the same key, which includes 1, 1.0, and True.

unique_keys.py
d = {'a': 1, 'a': 2}
print(d) # {'a': 2}, the last value wins
print({1: 'a', 1.0: 'b', True: 'c'}) # {1: 'c'}, all three are the same key

Dictionaries keep insertion order, but two dictionaries with the same pairs are equal regardless of order.

order_and_equality.py
print({'a': 1, 'b': 2} == {'b': 2, 'a': 1}) # True

reading_values.py
pizza = {'name': 'Margherita Pizza', 'price': 8.9}
print(pizza['name']) # 'Margherita Pizza'
print(pizza['size']) # KeyError: 'size'
print(pizza.get('size')) # None, no error
print(pizza.get('size', 'medium')) # 'medium', a custom fallback
print('price' in pizza) # True
print(8.9 in pizza) # False, `in` checks keys only, never values
print(8.9 in pizza.values()) # True

Choose the access style that matches your intent:

price = pizza['price']

Use when the key must exist. A KeyError is a useful signal that something is wrong.

d.setdefault(key, default) returns the value for key. If the key is missing, it inserts it with default first.

setdefault.py
pizza = {'name': 'Margherita'}
print(pizza.setdefault('size', 'medium')) # 'medium', inserted
print(pizza.setdefault('size', 'large')) # 'medium', already present, unchanged
print(pizza) # {'name': 'Margherita', 'size': 'medium'}

adding_and_updating.py
pizza = {'name': 'Margherita Pizza', 'price': 8.9}
pizza['price'] = 9.5 # Overwrites an existing key
pizza['vegan'] = False # Adds a new key
pizza.update({'price': 15.0, 'total_time': 25}) # Several at once
pizza.update(size='large') # Keyword form (string keys only)

When keys overlap, the right-hand value wins.

defaults = {'theme': 'light', 'lang': 'en'}
overrides = {'theme': 'dark'}
merged = defaults | overrides # {'theme': 'dark', 'lang': 'en'}
defaults |= overrides # In-place version

ToolBehavior
del d[key]Removes the key, and raises KeyError if it is missing
d.pop(key)Removes the key and returns its value, and raises KeyError if missing
d.pop(key, default)Same, but returns default instead of raising an error
d.popitem()Removes and returns the last inserted pair as a tuple
d.clear()Removes everything
removing_entries.py
pizza = {'name': 'Margherita', 'price': 8.9, 'time': 25}
price = pizza.pop('price') # 8.9, pizza is now {'name': ..., 'time': 25}
size = pizza.pop('size', None) # None, no error because a default was given
pizza.pop('size') # KeyError: 'size'
last = pizza.popitem() # ('time', 25)
del pizza['name'] # pizza is now {}

.keys(), .values(), and .items() return view objects. A view is a live window onto the dictionary, so it reflects later changes.

dictionary_views.py
d = {'a': 1}
keys = d.keys()
d['b'] = 2
print(keys) # dict_keys(['a', 'b']), updated automatically
print(list(d.items())) # [('a', 1), ('b', 2)]
print(list(d)) # ['a', 'b'], a list of the keys
# Key views support set operations
print({'a': 1, 'b': 2}.keys() & {'b': 9, 'c': 3}.keys()) # {'b'}

dict_iteration.py
products = {
'Laptop': 990,
'Smartphone': 600,
'Tablet': 250,
'Headphones': 70,
}
for item in products: # Keys
print(item)
for price in products.values(): # Values
print(price)
for item, price in products.items(): # Both
print(f'{item}: ${price}')
for index, (item, price) in enumerate(products.items(), start=1):
print(f'#{index} {item} -> ${price}')

Dictionaries stay in insertion order, so sort explicitly when you need a different order.

dict_sorting.py
for item in sorted(products): # Alphabetical by key
print(item)
by_price = sorted(products.items(), key=lambda kv: kv[1])
print(by_price)
# [('Headphones', 70), ('Tablet', 250), ('Smartphone', 600), ('Laptop', 990)]
print(dict(by_price)) # A new dict in price order
print(min(products, key=products.get)) # 'Headphones'
print(max(products, key=products.get)) # 'Laptop'

A comprehension builds a new dictionary in one expression: {key_expr: value_expr for item in iterable if condition}.

dict_comprehension.py
products = {'Laptop': 990, 'Smartphone': 600, 'Tablet': 250, 'Headphones': 70}
# Transform values: apply a 20% discount
discounted = {item: round(price * 0.8) for item, price in products.items()}
# {'Laptop': 792, 'Smartphone': 480, 'Tablet': 200, 'Headphones': 56}
# Filter: keep items above $200
premium_items = {
item: price
for item, price in products.items()
if price > 200
}
# {'Laptop': 990, 'Smartphone': 600, 'Tablet': 250}
# Swap keys and values (values must be unique and hashable)
inverted = {v: k for k, v in {'a': 1, 'b': 2}.items()} # {1: 'a', 2: 'b'}

Values can be dictionaries. Chain keys to reach into them, and chain .get() with {} defaults to avoid KeyError on missing levels.

nested_dictionaries.py
config = {'db': {'host': 'localhost', 'port': 5432}}
print(config['db']['port']) # 5432
print(config.get('cache', {}).get('ttl', 60)) # 60, no error even though 'cache' is missing
print(config['cache']['ttl']) # KeyError: 'cache'

Two of the most common dictionary jobs have dedicated tools in the standard library.

counting_and_grouping.py
from collections import Counter, defaultdict
# Counting by hand
counts = {}
for ch in 'banana':
counts[ch] = counts.get(ch, 0) + 1
print(counts) # {'b': 1, 'a': 3, 'n': 2}
# Counting with Counter
print(Counter('banana').most_common(1)) # [('a', 3)]
# Grouping with defaultdict
words = ['apple', 'kiwi', 'pear', 'banana']
groups = defaultdict(list)
for word in words:
groups[len(word)].append(word) # No need to check whether the key exists
print(dict(groups)) # {5: ['apple'], 4: ['kiwi', 'pear'], 6: ['banana']}

Adding or removing keys while iterating over a dictionary raises a RuntimeError.

mutation_during_iteration.py
products = {'Laptop': 990, 'Smartphone': 600, 'Tablet': 250, 'Headphones': 70}
for key in products:
if products[key] < 100:
del products[key] # RuntimeError: dictionary changed size during iteration
  1. Snapshot the keys. Loop over list(products), which is independent of the dictionary.

  2. Mutate the original safely. Deleting from the dictionary no longer affects the loop.

  3. Or skip mutation entirely. Build a new dictionary with a filtering comprehension. This is usually the cleanest option.

safe_mutation.py
# Option 1: iterate over a snapshot of the keys
for key in list(products):
if products[key] < 100:
del products[key]
# Option 2: build a new dictionary (preferred)
products = {k: v for k, v in products.items() if v >= 100}

copy_semantics.py
import copy
original = {'toppings': ['mozzarella', 'basil'], 'price': 10}
shallow = original.copy()
shallow['toppings'].append('mushrooms')
print(original['toppings']) # ['mozzarella', 'basil', 'mushrooms'], changed by accident
deep = copy.deepcopy(original)
deep['toppings'].append('oregano')
print(original['toppings']) # ['mozzarella', 'basil', 'mushrooms'], unaffected

Assigning a dictionary to a new name does not copy it at all. Both names point to the same object.


Big-O notation describes how an operation’s cost grows with the number of entries n. O(1) means the cost stays constant whether the dictionary has 5 entries or 5,000,000. Dictionaries achieve this with a hash table, which jumps straight to an entry instead of scanning for it.

OperationComplexityNote
d[key], d.get(key)O(1) FastAverage case
d[key] = valueO(1) FastAmortized, occasionally resizes the table
key in d, len(d)O(1) FastHash lookup
del d[key], d.pop(key)O(1) FastAverage case
d.update(other)O(k) Proportionalk is the size of other
Looping over .keys(), .values(), .items()O(n) LinearVisits every entry
value in d.values()O(n) LinearScans every value
d.copy()O(n) LinearBuilds a new table

MistakeProblemFix
d['missing']KeyErrord.get('missing', default) or check with in
if 8.9 in d: to search valuesin checks keys onlyif 8.9 in d.values():
Using a list as a keyTypeError: unhashable typeUse a tuple
Adding or removing keys while loopingRuntimeErrorLoop over list(d) or build a new dictionary
Expecting .copy() to copy nested objectsInner lists are sharedcopy.deepcopy(d)
dict.fromkeys(keys, [])Every key shares one list{k: [] for k in keys}
x = {} expecting an empty set{} is an empty dictionaryset()
Mixing 1, 1.0, and True as keysThey collapse into one keyUse consistent key types
for v in d.values(): v += 1Rebinds a local variable, the dictionary is unchangedfor k in d: d[k] += 1
d.get(k) or defaultReplaces valid falsy values such as 0d.get(k, default)
Assuming keys are sortedInsertion order is kept, not sorted ordersorted(d) or sorted(d.items())
  1. Use d[key] when a key must exist and d.get(key, default) when it is optional.
  2. Use .items() to loop over keys and values together.
  3. Use Counter for counting and defaultdict for grouping.
  4. Use comprehensions to transform or filter dictionaries instead of mutating them in a loop.
  5. Never change a dictionary’s size while iterating over it.
  6. Use copy.deepcopy() when nested mutable values must stay independent.
  7. Use tuples for composite keys, such as coordinates.
  8. Merge dictionaries with | (Python 3.9+) or {**a, **b}.