Dictionaries

You can think of lists as a mapping from indices to values. The indices are always integers and go from 0 to len(the_list) - 1, and the values are the items.

Dictionaries are collections, just like lists, but they have important differences:

  • lists map sequential numeric indices to items, whereas dictionaries can map most object types to any object,

  • lists are ordered collections of items, whereas dictionaries have no ordering.

Since anything can be used as index for an item, indices must be always specified when creating a dictionary:

[1]:
import math
[2]:
d = {1: 0.5, 'excellent index': math.sin, 0.1: 2}
d[1]
[2]:
0.5
[3]:
d['excellent index']
[3]:
<function math.sin(x, /)>
[4]:
d[0.1] = 3

The “indices” of a dictionary are called keys, and the things they map to are values. Together, each key-value pair is an item.

[5]:
d.keys()
[5]:
dict_keys([1, 'excellent index', 0.1])
[6]:
d.values()
[6]:
dict_values([0.5, <built-in function sin>, 3])
[7]:
d
[7]:
{1: 0.5, 'excellent index': <function math.sin(x, /)>, 0.1: 3}

As you can see, the values of a dictionary can be whatever we like, and need not be the same type of object.

You can see that the order of the keys, values and items we get back are not the same as the order we created the dictionary with. If you run the same example on your own you might get a different ordering. This is what we mean when we define dictionaries as unordered collections: when you iterate over its content, you cannot rely on the ordering.

It is however guaranteed that the n-th item returned by keys() corresponds to the n-th item returned by values(). This allows the following example to work flawlessly:

[8]:
for key, value in zip(d.keys(), d.values()):
    print(key, ':', value)
1 : 0.5
excellent index : <built-in function sin>
0.1 : 3

Of course, this could be considerably simpler just by using items(), which gives us tuples of key-value pairs.

[9]:
for key, value in d.items():
    print(key, ':', value)
1 : 0.5
excellent index : <built-in function sin>
0.1 : 3

We can create dictionaries from lists of 2-item lists.

[10]:
dict(enumerate(['a thing', 'another']))
[10]:
{0: 'a thing', 1: 'another'}

And also with dictionary comprehensions, in a similar manner to list comprehensions, with the additional specification of the key.

[11]:
{i: i**i for i in range(5) if i != 3}
[11]:
{0: 1, 1: 1, 2: 4, 4: 256}

Dictionary keys

There is no restriction on values a dictionary might hold, but there is on keys.

[12]:
numbers = [1, 4, 3]
[13]:
dd = {}
[14]:
dd[numbers] = 0
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[14], line 1
----> 1 dd[numbers] = 0

TypeError: unhashable type: 'list'

In essence, keys must not be mutable. This includes numbers, strings, and tuples, but not lists. This restriction is a trade-off that allows Python to make accessing values in a dictionary by key very fast.

Advanced (skip on first read) Immutable data types in Python have a __hash__() function, you can test it yourself:

[15]:
s = "a string"
s.__hash__()
[15]:
-96708273134104482

A hashing function creates an encoded (but not unique) representation of the object as a number. When you look up an item in a Python dictionary with my_dict["my_key"], what happens internally is:

  • hash of "my_key" is calculated,

  • this number is compared to every hash of every key in the dictionary, until a match between the hashes is found,

  • if two hashes match, and the two objects are really identical, the corresponding dictionary item is returned.

Looking up numbers instead of strings or tuples is considerably faster, but since two different strings can have the same hash, their content has to be compared as well to really tell whether they are equal. If two hashes are different on the other hand we are sure that the objects are different as well.

Iteration over dictionaries is over their keys.

[16]:
for key in d:
    print(key, ':', d[key])
1 : 0.5
excellent index : <built-in function sin>
0.1 : 3

We have already seen how to iterate over values (using d.values()) or keys and values simultaneously (using d.items()).

Exercise Alphabet mapping

Map each letter of the alphabet to a number with a dictionary comprehension, starting with 0 for a and ending with 25 for z.

You can get a string containing the letters of the alphabet like this:

[17]:
import string
[18]:
string.ascii_lowercase
[18]:
'abcdefghijklmnopqrstuvwxyz'

You can iterate over a string exactly like a list.

[19]:
for character in string.ascii_lowercase:
    print(character)
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z

Then, create the “reverse” dictionary, again with a comprehension, mapping letters to numbers.

Solution You need to have a list containing one number per letter, and to loop over that list along with the characters in the string. This is exactly the same as looping over items in a list alongside the index, so we can use enumerate.

[20]:
alphabet_map = {i: c for i, c in enumerate(string.ascii_lowercase)}

We can create the inverse map by swapping the key and value in the comprehension.

[21]:
reverse_map = {c: i for i, c in alphabet_map.items()}