Dictionaries and Sets
Looking up values by key with dictionaries, and keeping unique items with sets.
Dictionaries
A list finds items by position. Often, though, you want to find
something by a name: the capital of a country, the price of
a product, the settings of a user. That is what a
dictionary (type
dict) is for: a collection of key–value pairs,
where each key leads to one value.
A key is the name you look a value up by.
Within one dictionary every key is unique; values can repeat and can
be of any type. You write a dictionary as
key: value pairs between curly braces, or build one
with dict().
capitals = {"France": "Paris", "Japan": "Tokyo", "Kenya": "Nairobi"}
stock = dict(apples=12, pears=0)
phone_codes = dict([("NL", 31), ("BE", 32)])
empty = {}
print(capitals)
print(stock, phone_codes)
print(len(capitals), empty)
{'France': 'Paris', 'Japan': 'Tokyo', 'Kenya': 'Nairobi'}
{'apples': 12, 'pears': 0} {'NL': 31, 'BE': 32}
3 {}
If the same key appears twice in one literal, there is still only
one entry, holding the last value:
{"a": 1, "a": 2} is {"a": 2}.
Looking up values
Square brackets with a key return its value. If the key does not
exist, that is an error: Python raises KeyError. The
method get() is the forgiving alternative: it returns
None for a missing key, or a default you choose.
capitals = {"France": "Paris", "Japan": "Tokyo"}
print(capitals["Japan"])
print(capitals.get("Peru"))
print(capitals.get("Peru", "unknown"))
print(capitals["Peru"])
Tokyo None unknown KeyError: 'Peru'
Use d[key] when the key must be there and a missing key
is a bug you want to hear about. Use
d.get(key, default)
when a missing key is normal. A common pattern is counting:
words = "the cat and the hat".split()
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts)
{'the': 2, 'cat': 1, 'and': 1, 'hat': 1}
Adding, updating and deleting
Dictionaries are mutable. Assigning to d[key] adds the
pair if the key is new, and replaces the value if it already exists
— the same syntax does both. del d[key] removes a pair,
and pop(key) removes it and returns its value.
stock = {"apples": 12, "pears": 4}
stock["bananas"] = 30 # new key: added
stock["apples"] = 10 # existing key: updated
del stock["pears"]
print(stock)
sold_out = stock.pop("bananas")
print(sold_out, stock)
print(stock.pop("kiwis", 0)) # default instead of KeyError
{'apples': 10, 'bananas': 30}
30 {'apples': 10}
0
update() and setdefault()
update(other) copies all pairs from another dictionary
in, overwriting keys that already exist.
setdefault(key, default) returns the value of
key if it exists; if not, it first inserts
key with default and returns that.
settings = {"theme": "dark", "font_size": 14}
settings.update({"font_size": 16, "language": "en"})
print(settings)
print(settings.setdefault("theme", "light")) # exists: unchanged
print(settings.setdefault("autosave", True)) # missing: inserted
print(settings)
{'theme': 'dark', 'font_size': 16, 'language': 'en'}
dark
True
{'theme': 'dark', 'font_size': 16, 'language': 'en', 'autosave': True}
Other useful methods: clear() empties the dictionary,
copy() makes a
shallow copy, and popitem() removes and returns the most recently
added pair.
Merging with |
Since Python 3.9, the | operator merges two
dictionaries into a new one. When both have the same key, the
right-hand value wins. |= merges into the left
dictionary in place, like update().
defaults = {"theme": "light", "font_size": 14}
user = {"theme": "dark"}
merged = defaults | user
print(merged)
print(defaults) # unchanged
defaults |= user
print(defaults)
{'theme': 'dark', 'font_size': 14}
{'theme': 'light', 'font_size': 14}
{'theme': 'dark', 'font_size': 14}
Keys, values and items
Three methods give a view of a dictionary's contents:
keys(), values() and items(),
the last one as (key, value) tuples. A view is live: it
reflects later changes to the dictionary. Wrap it in
list() when you need a real list.
A plain for loop over a dictionary gives its keys. To
get both, loop over items() and
unpack
each pair.
prices = {"coffee": 2.5, "tea": 2.0, "cake": 3.75}
print(list(prices.keys()))
print(list(prices.values()))
print(list(prices.items()))
for item, price in prices.items():
print(f"{item}: {price:.2f}")
print("tea" in prices, 2.0 in prices, 2.0 in prices.values())
['coffee', 'tea', 'cake']
[2.5, 2.0, 3.75]
[('coffee', 2.5), ('tea', 2.0), ('cake', 3.75)]
coffee: 2.50
tea: 2.00
cake: 3.75
True False True
Since Python 3.7, a dictionary is guaranteed to remember the order in which keys were first inserted, and every loop and view follows that order. Updating the value of an existing key does not move it; deleting a key and adding it again puts it at the end.
What keys and values can be
Values can be anything: numbers, strings, lists, other dictionaries, functions. Keys are more restricted.
Keys must be hashable
To find a key fast, a dictionary computes a number from it, its
hash, and uses that number to decide where to store the
pair. An object is hashable when it
has a hash value that never changes during its lifetime, and can be
compared with other objects for equality. In practice: immutable
built-in values — numbers, strings, None, and tuples
that contain only hashable items — are hashable. Mutable containers
such as lists, dictionaries and sets are not, because their contents
(and so their hash) could change after they were stored.
locations = {}
locations[(52.37, 4.90)] = "Amsterdam" # a tuple is fine
print(locations)
locations[[52.37, 4.90]] = "Amsterdam" # a list is not
{(52.37, 4.9): 'Amsterdam'}
TypeError: unhashable type: 'list'
(Newer Python versions word this error message a little differently,
but the exception is still a
TypeError.)
Nested dictionaries
Since values can be anything, a dictionary can contain other dictionaries and lists. This is how structured data such as JSON is usually represented in Python. Chain the lookups to reach an inner value.
users = {
"ada": {"name": "Ada Lovelace", "languages": ["Python", "C"]},
"alan": {"name": "Alan Turing", "languages": ["Python"]},
}
print(users["ada"]["name"])
users["alan"]["languages"].append("Go")
print(users["alan"])
for username, info in users.items():
print(username, len(info["languages"]))
Ada Lovelace
{'name': 'Alan Turing', 'languages': ['Python', 'Go']}
ada 2
alan 2
Sets
A set is an unordered collection of
unique, hashable items — like the keys of a dictionary without the
values. Adding an item that is already present has no effect, so
set(some_list) is the quickest way to remove
duplicates. You write a set with curly braces, or build one from any
iterable with set().
rolls = [3, 6, 3, 1, 6, 6, 2]
unique = set(rolls)
print(unique, len(unique))
tags = {"python", "linux", "python", "network"}
print(sorted(tags))
empty = set()
print(type({}), type(empty))
{1, 2, 3, 6} 4
['linux', 'network', 'python']
<class 'dict'> <class 'set'>
A set has no order, so it has no indexes: tags[0] is a
TypeError. The order in which a set of strings prints
can even differ from one run of the program to the next. That is why
the example sorts the tags before printing them.
Adding and removing items
add(x) adds one item and update(iterable)
adds several. To remove an item there are two methods:
remove(x) raises KeyError if
x is missing, while discard(x) silently
does nothing. pop() removes and returns an arbitrary
item, and clear() empties the set.
online = {1, 2, 5}
online.add(3)
online.add(2) # already there: no effect
online.discard(7) # not there: no error
online.remove(5)
print(online)
online.remove(7) # not there: KeyError
{1, 2, 3}
KeyError: 7
Fast membership tests
x in some_list compares x with the items
one by one, so it gets slower as the list grows. A set (like a
dictionary) uses the hash of x to go straight to the
place where it would be stored, so x in some_set takes
about the same time however large the set is. When you check
membership often — allowed users, seen IDs, blocked addresses —
store the values in a set.
Set operations
Sets support the operations from mathematics. Each has an operator and a method with a readable name; the methods also accept any iterable, the operators only sets and frozensets.
| Operator | Method | Result |
|---|---|---|
a | b |
a.union(b) |
items in a or b (or both) |
a & b |
a.intersection(b) |
items in both a and b |
a - b |
a.difference(b) |
items in a but not in b |
a ^ b |
a.symmetric_difference(b) |
items in exactly one of the two |
a <= b |
a.issubset(b) |
True if every item of a is in
b
|
python_team = {1, 2, 3, 4} # employee IDs
linux_team = {3, 4, 5}
print(python_team | linux_team)
print(python_team & linux_team)
print(python_team - linux_team)
print(python_team ^ linux_team)
print({3, 4} <= python_team)
{1, 2, 3, 4, 5}
{3, 4}
{1, 2}
{1, 2, 5}
True
Of the four operations that combine sets, difference is the only one
where the order of the operands matters:
linux_team - python_team is {5}.
frozenset
A set is mutable, and therefore not hashable: it cannot be a
dictionary key or an item of another set. A
frozenset is the immutable version
of a set. It supports all the operations that do not change it —
in, len(), union, intersection and so on —
but has no add() or remove(). In return,
it is hashable. Two frozensets with the same items are equal,
whatever order you listed them in.
weekend = frozenset({6, 7})
print(weekend)
print(6 in weekend)
opening_hours = {weekend: "closed"} # usable as a key
print(opening_hours[frozenset({7, 6})])
weekend.add(5)
frozenset({6, 7})
True
closed
AttributeError: 'frozenset' object has no attribute 'add'