Dictionaries

Beginner40 min

Map keys to values with dicts, look things up safely, merge dicts with the pipe operator, and preview the new immutable frozendict.

Builds on
What you will learn
  • Create dicts and read, add, update and delete entries
  • Look up keys safely with get, setdefault and in
  • Iterate over keys, values and items
  • Merge dicts with the | and |= operators
On this page

A list finds things by position. A dictionary finds them by name: look up "Ada" and get her age, look up a word and get its definition. Dicts are among the most used structures in Python, and the language itself leans on them everywhere, from keyword arguments to object attributes. The shared fundamentals lesson on maps and sets explains the idea in the abstract. Here is how Python does it.

Creating and using a dict

Curly braces with key: value pairs make a dict. Assigning to a key either adds a new entry or replaces an existing one. Each key appears once.

dict_basics.py
ages = {"Ada": 36, "Alan": 41}
ages["Grace"] = 85
ages["Ada"] = 37
print(ages, len(ages))
print(ages["Alan"])
Output
{'Ada': 37, 'Alan': 41, 'Grace': 85} 3
41

Keys must be hashable, which in practice means immutable things such as strings, numbers and tuples. Values can be anything.

Safe lookups

ages["Linus"] raises KeyError when the key is missing:

ages = {"Ada": 37}
print(ages["Linus"])
Output
KeyError: 'Linus'

That is right when a missing key is a bug, but often it is just a case to handle. Three tools let you look without crashing:

  • d.get(key) returns None if the key is absent, and d.get(key, default) returns your default instead. It never changes the dict.
  • key in d tests for a key.
  • d.setdefault(key, default) returns the value if the key exists, and otherwise stores the default and returns it.
dict_get.py
ages = {"Ada": 37, "Alan": 41}
print(ages.get("Linus"))
print(ages.get("Linus", 0))
print("Ada" in ages, "Linus" in ages)
Output
None
0
True False

setdefault makes the grouping idiom a single line: fetch the list for this letter (creating an empty one the first time) and append to it.

dict_setdefault.py
groups = {}
for word in ["apple", "avocado", "banana", "blueberry", "cherry"]:
groups.setdefault(word[0], []).append(word)
print(groups)
Output
{'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}

To remove entries, use del d[key] (raises KeyError if absent) or d.pop(key), which also returns the value. pop(key, default) stays quiet when the key is missing.

dict_remove.py
ages = {"Ada": 37, "Alan": 41, "Grace": 85}
del ages["Alan"]
print(ages.pop("Grace"), ages)
print(ages.pop("Nobody", "not there"))
Output
85 {'Ada': 37}
not there

Iterating

A dict remembers the order in which keys were first inserted, and loops follow that order. for key in d visits keys, d.values() gives the values, and d.items() gives (key, value) pairs, which you normally unpack in the loop header.

dict_loop.py
stock = {"pens": 12, "ink": 3, "paper": 40}
for key in stock:
print(key, end=" ")
print()
for item, count in stock.items():
print(f"{item}: {count}")
Output
pens ink paper
pens: 12
ink: 3
paper: 40

These “views” show the dict’s current contents. Wrap them in list(...) when you want a snapshot. reversed(stock) walks the keys from last to first.

dict_views.py
stock = {"pens": 12, "ink": 3, "paper": 40}
print(list(stock.keys()), list(stock.values()))
for key in reversed(stock):
print(key, end=" ")
print()
Output
['pens', 'ink', 'paper'] [12, 3, 40]
paper ink pens

Do not add or delete keys while looping over the dict, because Python raises a RuntimeError.

Merging dicts

Requires version 3.9 or later a | b builds a new dict containing the entries of both. When a key appears in both, the right-hand value wins, so layering user settings over defaults is one expression.

dict_merge.py
defaults = {"theme": "light", "font": 12, "lang": "en"}
user = {"theme": "dark", "font": 14}
merged = defaults | user
print(merged)
print(defaults)
Output
{'theme': 'dark', 'font': 14, 'lang': 'en'}
{'theme': 'light', 'font': 12, 'lang': 'en'}

a |= b updates a in place, like a.update(b):

dict_merge_inplace.py
defaults = {"theme": "light", "font": 12, "lang": "en"}
defaults |= {"lang": "fr"}
print(defaults)
print({"a": 1} | {"a": 2})
Output
{'theme': 'light', 'font': 12, 'lang': 'fr'}
{'a': 2}

These operators arrived in Python 3.9 (PEP 584). On older versions, {**a, **b} does the same job as a | b; the double star spreads each dict’s entries into the new one, just as * spreads a list.

dict_merge_old.py
defaults = {"theme": "light", "font": 12}
user = {"theme": "dark"}
print({**defaults, **user})
Output
{'theme': 'dark', 'font': 12}

frozendict, an immutable dict

Requires version 3.15 or later Python 3.15 added a built-in frozendict (PEP 814). It looks like a dict but cannot be changed after it is created, which makes it hashable whenever its keys and values are. This section needs Python 3.15; on 3.14 or earlier the name does not exist and you get a NameError.

frozendict_demo.py
config = frozendict(host="localhost", port=8080)
print(config)
print(config["port"], config.get("debug", False))
Output
frozendict({'host': 'localhost', 'port': 8080})
8080 False

Assigning to it fails:

config = frozendict(host="localhost", port=8080)
config["port"] = 9090
Output
TypeError: 'frozendict' object does not support item assignment

Order does not affect equality or hashing, and a frozendict can be a dict key, which an ordinary dict cannot:

frozendict_hash.py
config = frozendict(host="localhost", port=8080)
other = frozendict(port=8080, host="localhost")
print(config == other, hash(config) == hash(other))
cache = {config: "connection #1"}
print(cache[other])
Output
True True
connection #1

Two more facts to remember. A frozendict is not a subclass of dict, so isinstance(x, dict) is false for it. And since it cannot be modified, “changing” one means building a new one.

frozendict_update.py
config = frozendict(host="localhost", port=8080)
print(isinstance(config, dict))
updated = frozendict({**config, "port": 9090})
print(updated)
print(config)
Output
False
frozendict({'host': 'localhost', 'port': 9090})
frozendict({'host': 'localhost', 'port': 8080})

For code that must run on older versions, stick to a plain dict and simply do not mutate it.

Common pitfalls

  • KeyError from d[key]. Use get, in, or setdefault when the key may be missing.
  • Unhashable keys. A list or another dict cannot be a key. Convert a list to a tuple first.
  • Mutating while iterating. Adding or removing keys inside for k in d raises RuntimeError. Loop over list(d) instead.
  • Expecting | to modify its operands. a | b returns a new dict. Use |= to update a.
  • Mixing up {}. An empty {} is an empty dict, not an empty set. Use set() for a set.
  • Assuming get stores the default. It does not. Only setdefault stores it.
ExercisePredict the dict operations

Predict the four lines this prints, then run the file. Input: none. Expected output: four lines.

predict_dict.py
stock = {"pens": 12, "ink": 3}
print(stock.get("paper", 0))
print(stock.pop("ink"))
print(stock)
print("ink" in stock)
Show solution
0
3
{'pens': 12}
False

get returns the default 0 for the missing key and does not add it. pop removes ink and hands back its value, 3. After that the dict only has pens, so "ink" in stock is false.

ExerciseWord-frequency counter

Count how often each word occurs in a text, ignoring case. Print every word that occurs more than once, in alphabetical order, with its count, and then print the number of distinct words.

Input: the two lines of text below. Expected output:

dog 2
fox 2
the 4
12 distinct words
text = """the quick brown fox jumps over the lazy dog
The dog barks and the fox runs away"""
Show solution
word_freq.py
text = """the quick brown fox jumps over the lazy dog
The dog barks and the fox runs away"""
counts = {}
for word in text.lower().split():
counts[word] = counts.get(word, 0) + 1
for word in sorted(counts):
if counts[word] > 1:
print(word, counts[word])
print(len(counts), "distinct words")
Output
dog 2
fox 2
the 4
12 distinct words

counts.get(word, 0) + 1 reads “the current count, or zero if we have not seen it”. Looping over sorted(counts) visits the keys in alphabetical order. max(counts, key=counts.get) would give you the single most common word.

ExerciseInvert a dictionary

Swap keys and values in colors = {"apple": "red", "cherry": "red", "banana": "yellow", "lime": "green"}. Several keys may share a value, so each color should map to a list of fruits. Print the result.

Input: the colors dict. Expected output: {'red': ['apple', 'cherry'], 'yellow': ['banana'], 'green': ['lime']}.

Show solution
invert_dict.py
colors = {"apple": "red", "cherry": "red", "banana": "yellow", "lime": "green"}
inverted = {}
for fruit, color in colors.items():
inverted.setdefault(color, []).append(fruit)
print(inverted)
Output
{'red': ['apple', 'cherry'], 'yellow': ['banana'], 'green': ['lime']}

setdefault creates the empty list the first time a color appears, so one line handles both the first and later fruits.

Summary

  • A dict maps hashable keys to values and iterates in insertion order. Use get, in and setdefault for safe lookups.
  • Requires version 3.9 or later a | b merges into a new dict and a |= b updates in place. The right side wins on clashes.
  • Requires version 3.15 or later frozendict is an immutable, hashable mapping that is not a dict subclass.

Next: sets.