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.
At a Glance
Section titled “At a Glance”| Task | Syntax | Example |
|---|---|---|
| Create | {key: value}, dict(...) | {'name': 'Ann', 'age': 31} |
| Read | d[key], d.get(key, default) | d.get('city', 'Unknown') |
| Add or change | d[key] = value | d['age'] = 32 |
| Merge | d.update(other) | d.update({'age': 33}) |
| Remove | del d[key], d.pop(key, default) | d.pop('age', None) |
| Check a key | key in d | 'name' in d |
| Size | len(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
Section titled “Creating Dictionaries”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.
Keys Must Be Hashable Rule
Section titled “Keys Must Be Hashable ”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.
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 tooKeys Are Unique
Section titled “Keys Are Unique”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.
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 keyOrder and Equality
Section titled “Order and Equality”Dictionaries keep insertion order, but two dictionaries with the same pairs are equal regardless of order.
print({'a': 1, 'b': 2} == {'b': 2, 'a': 1}) # TrueReading Values
Section titled “Reading Values”pizza = {'name': 'Margherita Pizza', 'price': 8.9}
print(pizza['name']) # 'Margherita Pizza'print(pizza['size']) # KeyError: 'size'
print(pizza.get('size')) # None, no errorprint(pizza.get('size', 'medium')) # 'medium', a custom fallback
print('price' in pizza) # Trueprint(8.9 in pizza) # False, `in` checks keys only, never valuesprint(8.9 in pizza.values()) # TrueChoose 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.
size = pizza.get('size', 'medium')Use when the key is optional and a fallback value makes sense.
if 'size' in pizza: print(pizza['size'])Use when you need to run different logic depending on whether the key exists.
setdefault()
Section titled “setdefault()”d.setdefault(key, default) returns the value for key. If the key is missing, it inserts it with default first.
pizza = {'name': 'Margherita'}
print(pizza.setdefault('size', 'medium')) # 'medium', insertedprint(pizza.setdefault('size', 'large')) # 'medium', already present, unchangedprint(pizza) # {'name': 'Margherita', 'size': 'medium'}Adding, Updating, and Merging
Section titled “Adding, Updating, and Merging”pizza = {'name': 'Margherita Pizza', 'price': 8.9}
pizza['price'] = 9.5 # Overwrites an existing keypizza['vegan'] = False # Adds a new keypizza.update({'price': 15.0, 'total_time': 25}) # Several at oncepizza.update(size='large') # Keyword form (string keys only)Merging Two Dictionaries
Section titled “Merging Two Dictionaries”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 versionmerged = {**defaults, **overrides} # {'theme': 'dark', 'lang': 'en'}merged = defaults.copy()merged.update(overrides)Removing Entries
Section titled “Removing Entries”| Tool | Behavior |
|---|---|
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 |
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 givenpizza.pop('size') # KeyError: 'size'
last = pizza.popitem() # ('time', 25)del pizza['name'] # pizza is now {}Dictionary Views
Section titled “Dictionary Views”.keys(), .values(), and .items() return view objects. A view is a live window onto the dictionary, so it reflects later changes.
d = {'a': 1}keys = d.keys()
d['b'] = 2print(keys) # dict_keys(['a', 'b']), updated automaticallyprint(list(d.items())) # [('a', 1), ('b', 2)]print(list(d)) # ['a', 'b'], a list of the keys
# Key views support set operationsprint({'a': 1, 'b': 2}.keys() & {'b': 9, 'c': 3}.keys()) # {'b'}Iteration
Section titled “Iteration”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}')Sorting and Finding Extremes
Section titled “Sorting and Finding Extremes”Dictionaries stay in insertion order, so sort explicitly when you need a different order.
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 orderprint(min(products, key=products.get)) # 'Headphones'print(max(products, key=products.get)) # 'Laptop'Dictionary Comprehensions
Section titled “Dictionary Comprehensions”A comprehension builds a new dictionary in one expression: {key_expr: value_expr for item in iterable if condition}.
products = {'Laptop': 990, 'Smartphone': 600, 'Tablet': 250, 'Headphones': 70}
# Transform values: apply a 20% discountdiscounted = {item: round(price * 0.8) for item, price in products.items()}# {'Laptop': 792, 'Smartphone': 480, 'Tablet': 200, 'Headphones': 56}
# Filter: keep items above $200premium_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'}Nested Dictionaries
Section titled “Nested Dictionaries”Values can be dictionaries. Chain keys to reach into them, and chain .get() with {} defaults to avoid KeyError on missing levels.
config = {'db': {'host': 'localhost', 'port': 5432}}
print(config['db']['port']) # 5432print(config.get('cache', {}).get('ttl', 60)) # 60, no error even though 'cache' is missingprint(config['cache']['ttl']) # KeyError: 'cache'Counting and Grouping
Section titled “Counting and Grouping”Two of the most common dictionary jobs have dedicated tools in the standard library.
from collections import Counter, defaultdict
# Counting by handcounts = {}for ch in 'banana': counts[ch] = counts.get(ch, 0) + 1print(counts) # {'b': 1, 'a': 3, 'n': 2}
# Counting with Counterprint(Counter('banana').most_common(1)) # [('a', 3)]
# Grouping with defaultdictwords = ['apple', 'kiwi', 'pear', 'banana']groups = defaultdict(list)for word in words: groups[len(word)].append(word) # No need to check whether the key existsprint(dict(groups)) # {5: ['apple'], 4: ['kiwi', 'pear'], 6: ['banana']}Changing a Dictionary While Looping
Section titled “Changing a Dictionary While Looping”Adding or removing keys while iterating over a dictionary raises a RuntimeError.
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-
Snapshot the keys. Loop over
list(products), which is independent of the dictionary. -
Mutate the original safely. Deleting from the dictionary no longer affects the loop.
-
Or skip mutation entirely. Build a new dictionary with a filtering comprehension. This is usually the cleanest option.
# Option 1: iterate over a snapshot of the keysfor 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}Copying Dictionaries
Section titled “Copying Dictionaries”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'], unaffectedAssigning a dictionary to a new name does not copy it at all. Both names point to the same object.
Time Complexity
Section titled “Time Complexity”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.
| Operation | Complexity | Note |
|---|---|---|
d[key], d.get(key) | O(1) Fast | Average case |
d[key] = value | O(1) Fast | Amortized, occasionally resizes the table |
key in d, len(d) | O(1) Fast | Hash lookup |
del d[key], d.pop(key) | O(1) Fast | Average case |
d.update(other) | O(k) Proportional | k is the size of other |
Looping over .keys(), .values(), .items() | O(n) Linear | Visits every entry |
value in d.values() | O(n) Linear | Scans every value |
d.copy() | O(n) Linear | Builds a new table |
Common Mistakes
Section titled “Common Mistakes”| Mistake | Problem | Fix |
|---|---|---|
d['missing'] | KeyError | d.get('missing', default) or check with in |
if 8.9 in d: to search values | in checks keys only | if 8.9 in d.values(): |
| Using a list as a key | TypeError: unhashable type | Use a tuple |
| Adding or removing keys while looping | RuntimeError | Loop over list(d) or build a new dictionary |
Expecting .copy() to copy nested objects | Inner lists are shared | copy.deepcopy(d) |
dict.fromkeys(keys, []) | Every key shares one list | {k: [] for k in keys} |
x = {} expecting an empty set | {} is an empty dictionary | set() |
Mixing 1, 1.0, and True as keys | They collapse into one key | Use consistent key types |
for v in d.values(): v += 1 | Rebinds a local variable, the dictionary is unchanged | for k in d: d[k] += 1 |
d.get(k) or default | Replaces valid falsy values such as 0 | d.get(k, default) |
| Assuming keys are sorted | Insertion order is kept, not sorted order | sorted(d) or sorted(d.items()) |
Best Practices Checklist
Section titled “Best Practices Checklist”- Use
d[key]when a key must exist andd.get(key, default)when it is optional. - Use
.items()to loop over keys and values together. - Use
Counterfor counting anddefaultdictfor grouping. - Use comprehensions to transform or filter dictionaries instead of mutating them in a loop.
- Never change a dictionary’s size while iterating over it.
- Use
copy.deepcopy()when nested mutable values must stay independent. - Use tuples for composite keys, such as coordinates.
- Merge dictionaries with
|(Python 3.9+) or{**a, **b}.