Libraries.

Beginner

What are dictionnaries and why use them.


In more technical termes, dictionnaries are defined as :

  • Mutable.
  • Unordered.
  • Subscriptable / Not slicable.
  • Does not accepts duplicate elements.

Dictionnaries are a little different from other collections.

Their use cases are easier to spot, they work with keys and every task where you would need to link a value to a key can be addressed with dictionnaries.

ToC

In [1]:
import copy
import itertools as it
from collections import defaultdict

Beginner

Declaring a dictionary.


Contrary to other collections, there is only one way to declare a dictionary.

ToC

In [2]:
d1 = dict()

It is not possible to declare an empty dictionary with d1 = {} as this notation is reserverd for sets ...


... But there are two ways to declare a dictionary with values.

In [3]:
# The most used version is :
d1 = {'A': 1, 'B': 2}

# But we can also use :
d1 = dict([('A', 1), ('B', 2)])

The second version works with any combination of iterables, as long as it is a "list" of "pairs" of values.

For each pair, the first value become a key, the second is the value attached to it.


A dictionary can also be initialized with only keys.

In [4]:
d1 = dict.fromkeys(['first', 'second'])
print(d1)
{'first': None, 'second': None}

Intermediate

The zip built-in function allows to create a dictionary from two lists.

It simply creates a list a tuples with each pairs of elements from the two lists.

In [5]:
keys_list = ['A', 'B', 'C']
values_list = [1, 2, 3]

zipped = zip(keys_list, values_list)
lst_zip = list(zipped)

print(lst_zip)
[('A', 1), ('B', 2), ('C', 3)]

At this point we can easly convert this object to a dictionary.

In [6]:
d1 = dict(lst_zip)
print(d1)
{'A': 1, 'B': 2, 'C': 3}

This can be achieved in one line.

In [7]:
d1 = dict(list(zip(keys_list, values_list)))
print(d1)
{'A': 1, 'B': 2, 'C': 3}

The itertools.ziplongest function allows to work with unevenly sized lists.

In [8]:
keys_list = ['A', 'B', 'C', 'D', 'E']
values_list = [1, 2, 3]

lst_zip = list(it.zip_longest(keys_list, values_list))
d1 = dict(lst_zip)
print(d1)
{'A': 1, 'B': 2, 'C': 3, 'D': None, 'E': None}

Finaly, it should be noted that keys and values can be of any types, even iterables allowing to create really complex structures with dictionaries.

In [9]:
d1 = {'A': [1, 2], 2: 3, 5.1: 'value'}

the only constraint is that keys must be unique.

Beginner

Accessing elements in a dictionary.


Dictionary are made to easly retrieve a value from a key.

There are several ways to do that, some with default values, some with other features.

ToC

We can access any value with its key with the [] notation.

In [10]:
d1 = {'A': 'word', 'B': [1, 2], 'C': 3}

print(d1['A'])
word

The dict.get method can also be useful if we are not sure if the key exists.

In [11]:
print("Original dictionary :")
print(d1)
Original dictionary :
{'A': 'word', 'B': [1, 2], 'C': 3}

Using dict.get on an existing item.

In [12]:
print(d1.get('A', 'default value'))
word

Using dict.get on an item that does not existing in the dictionary.

In [13]:
print(d1.get('D', 'default value'))
default value

When searching for 'D', which is not a key in the dictionary, the get method returns 'default value' but the dictionary itself is not altered.


Another possibility is the setdefault function.

In [14]:
print(d1.setdefault('D', 'default value'))
default value

Just like with get, the default value is returned if the searched key is not in the dictionary, but this time the key is added to the dictionary.

In [15]:
print(d1)
{'A': 'word', 'B': [1, 2], 'C': 3, 'D': 'default value'}

When searching for 'D', which is not a key in the dictionary, the setdefault methodreturns 'default value' and the value is adde to the dictionary itself with 'D' as a key.

We can easly check if a key is in the dictionary.

In [16]:
print('A' in d1)
True

The dict.items method returns a dict_items object that act as a list of tuples.

Each tuple is a pair of key with the associated value.

In [17]:
print(d1.items())
dict_items([('A', 'word'), ('B', [1, 2]), ('C', 3), ('D', 'default value')])

The dict.keys method returns a dict_keys object that act as a list.

This list contains all keys in the first level of the dictionary.


A list of keys can also be obtained with list(d1).

In [18]:
print(d1.keys())
dict_keys(['A', 'B', 'C', 'D'])

The dict.values method returns a dict_values object that act as a list.

This list contains all values in the first level of the dictionary.

In [19]:
print(d1.values())
dict_values(['word', [1, 2], 3, 'default value'])

Beginner

Adding elements to a dictionary.


Only the dict.update method (same with sets) is available to add elements to dictionaries.

ToC

In [20]:
d1 = {'A': 1, 'B': 2}

d1.update({'C': 3})

print(d1)
{'A': 1, 'B': 2, 'C': 3}

Just like before, we can use any iterable shaped like a "list of pairs".

In [21]:
d1.update([('D', 4), ('E', 5)])

print(d1)
{'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5}

Beginner

Modifying elements in a dictionary.


Since we can access values, we can modify it just as easily as with other mutable collections.

ToC

In [22]:
d1 = {'A': 1, 'B': 2}

print('Original dictionary.')
print(d1)
Original dictionary.
{'A': 1, 'B': 2}

Now let's change the value linked to the 'A' key.

In [23]:
d1['A'] = 'New value'

print('\nDictionary after modification.')
print(d1)
Dictionary after modification.
{'A': 'New value', 'B': 2}

Intermediate

We cannot modify a key direclty but there are some workarounds.

In [24]:
d1 = {'A': 1, 'B': 2}

d1['new_A'] = d1.get('A')
del d1['A']

print(d1)
{'B': 2, 'new_A': 1}

Basically we just create a new key with the value of the old key and we delete the old item.

Beginner

Deleting elements from a dictionary.


Just like accessing, we can delete a key-value pair if we know the key.

ToC

In [25]:
d1 = {'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6}

The dict.pop method remove the key-value pair passing the key as a parameter.

The value is returned.

In [26]:
print('Item return by pop :')
print(d1.pop('A'))

print('\nState of the dictionary :')
print(d1)
Item return by pop :
1

State of the dictionary :
{'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6}

The dict.popitem method remove the last key-value pair in the dictionary.

The key-value pair is returned.

In [27]:
print('Item return by popitem :')
print(d1.popitem())

print('\nState of the dictionary :')
print(d1)
Item return by popitem :
('F', 6)

State of the dictionary :
{'B': 2, 'C': 3, 'D': 4, 'E': 5}

The del statement deletes a specified key and the associated value.

Nothing is returned.

In [28]:
del d1['E']

print(d1)
{'B': 2, 'C': 3, 'D': 4}

the dict.clear method removes everything in a dictionary.

In [29]:
d1.clear()
print(d1)
{}

Beginner

Operations with dictionaries.


Dictionaries do not support as many operators as sets but we can still perform a union as of version 3.9.

ToC

In [30]:
d1 = {'A': 10, 'B': 15}
d2 = {'C': 30}

d3 = d1 | d2

the |= operator updates the dictionary on the left of the operator.

In [31]:
print(d3)
{'A': 10, 'B': 15, 'C': 30}

Beginner

Copying a dictionary.


Copies work just like they do for lists and other mutable collections.

ToC

In [32]:
d1 = {'A': 1, 'B': [1, 2]}

Dictionary have a built-in method that returns a shallow copy.

In [33]:
d2 = d1.copy()

d2['A'] = 2
print('The original dictionary after modifying the first level of the copied dictionary.')
print(d1)
The original dictionary after modifying the first level of the copied dictionary.
{'A': 1, 'B': [1, 2]}
In [34]:
d2['B'][0] = 3
print('The original dictionary after modifying a deeper level of the copied dictionary.')
print(d1)
The original dictionary after modifying a deeper level of the copied dictionary.
{'A': 1, 'B': [3, 2]}

Intermediate

It is possible to make a "real" copy.

In [35]:
d2 = copy.deepcopy(d1)

Advanced

We can illustrate that by accessing the memories addresses of these objects.

In [36]:
d1 = {'A': 1, 'B': [1, 2]}
d2 = d1.copy()

print(f"Address of the original dictionary : {hex(id(d1))}")
print(f"Address of the copied dictionary   : {hex(id(d2))}")

print(f"\nAddress of the deeper level of the original dictionary : {hex(id(d1['B']))}")
print(f"Address of the deeper level of the copied dictionary   : {hex(id(d2['B']))}")
Address of the original dictionary : 0x7fa90c9fc300
Address of the copied dictionary   : 0x7fa90c9fbf40

Address of the deeper level of the original dictionary : 0x7fa90c9faa40
Address of the deeper level of the copied dictionary   : 0x7fa90c9faa40

Same with deepcopies.

In [37]:
d1 = {'A': 1, 'B': [1, 2]}
d2 = copy.deepcopy(d1)

print(f"Address of the original dictionary : {hex(id(d1))}")
print(f"Address of the copied dictionary   : {hex(id(d2))}")

print(f"\nAddress of the deeper level of the original dictionary : {hex(id(d1['B']))}")
print(f"Address of the deeper level of the copied dictionary   : {hex(id(d2['B']))}")
Address of the original dictionary : 0x7fa90c9ff500
Address of the copied dictionary   : 0x7fa90c9bdb80

Address of the deeper level of the original dictionary : 0x7fa90c9fa980
Address of the deeper level of the copied dictionary   : 0x7fa90c9fd640

Now the addresses are all different.

Copying part of a dictionary works in the same fashion, some elements can be copied directly, some do not

In [38]:
d1 = {'A': 500, 'B': [250, 300]}
elt_1 = d1['A']

print(f"Address of the original element : {hex(id(d1['A']))}.")
print(f"Address of the copied element   : {hex(id(elt_1))}.")
Address of the original element : 0x7fa90c991950.
Address of the copied element   : 0x7fa90c991950.
In [39]:
print("\nAssigning a new value to the copied element.\n")
elt_1 = 501
Assigning a new value to the copied element.

In [40]:
print(f"Address of the original element : {hex(id(d1['A']))}, and its value : {d1['A']}.")
print(f"Address of the copied element   : {hex(id(elt_1))}, and its value : {elt_1}.")
Address of the original element : 0x7fa90c991950, and its value : 500.
Address of the copied element   : 0x7fa90c991890, and its value : 501.

At first we can see that the "copy" (the elt_1 variable) share the same address as the element directly in the dictionary. We could think that it is not a copy.

But when we try to change this value, we can see that the address of the copied value changed and the actual value inside the dictionary remained the same.


This come from the fact that python base types (int, float, str, etc.) are immutable. We cannot change directly the value of an integer, python have to create a new variable first.


Now we can do the same thing but with a mutable object such as a list.

In [41]:
elt_2 = d1['B']

print(f"Address of the original element : {hex(id(d1['B']))}")
print(f"Address of the copied element   : {hex(id(elt_2))}")
Address of the original element : 0x7fa90c9f9d40
Address of the copied element   : 0x7fa90c9f9d40
In [42]:
print("\nAssigning a new value to the copied element.\n")
elt_2[0] = 251

# %%s
print(f"Address of the original element : {hex(id(d1['B']))}, and its value : {d1['B']}.")
print(f"Address of the copied element   : {hex(id(elt_2))}, and its value : {elt_2}.")
Assigning a new value to the copied element.

Address of the original element : 0x7fa90c9f9d40, and its value : [251, 300].
Address of the copied element   : 0x7fa90c9f9d40, and its value : [251, 300].

We observe the same behaviour as when we try to copy a regular list. The list is not copied but a reference to it is. If we need a copy of this list we must use the copy.copy or the copy.deepcopy functions from the copy librairy.

Beginner

Iterating through a dictionary.


As seen before, the values, keys and items methods return lists of elements which allow to iterate through them.

ToC

In [43]:
d1 = {'A': 1, 'B': 2, 'C': 3}

Iterating through values.

In [44]:
for x in d1.values():
    print(x)
1
2
3

Iterating through keys.

In [45]:
for x in d1.keys():
    print(x)
A
B
C

Iterating through items (key-value pairs).

In [46]:
for x in d1.items():
    print(x)
('A', 1)
('B', 2)
('C', 3)

Intermediate

The iter function can also be used to get an iterator over the keys of the dictionary.

In [47]:
for x in iter(d1):
    print(x)
A
B
C

Beginner

Logic tests with dictionaries.


Just like with lists, a dictionary can be tested and is Truthy if not empty.

ToC

In [48]:
d1 = {'A': 1, 'B': 2}

while d1:
    item = d1.popitem()
    print(f"key {item[0]} -> value {item[1]}")
key B -> value 2
key A -> value 1

Intermediate

Dictionary comprehension.


Dictionary comprehensions work in a similar fashion as with other iterables.

ToC

In [49]:
l1 = [10, 15, 'E', [1, 'K'], True]

d1 = {f"elt_{i}": x for i, x in enumerate(l1, start=1)}
print(d1)
{'elt_1': 10, 'elt_2': 15, 'elt_3': 'E', 'elt_4': [1, 'K'], 'elt_5': True}

Intermediate

Defaultdicts.


Let's learn with an example.

ToC

In [50]:
l1 = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
dd1 = defaultdict(list)

for k, v in l1:
    dd1[k].append(v)

print(dd1.items())
dict_items([('yellow', [1, 3]), ('blue', [2, 4]), ('red', [1])])

In this example, in the first iteration of the loop, we try to access dd1['yellow'] which would fail with a normal dictionary since this key does not exist.

The definition dd1 = defaultdict(list) allows to initialize by default every unkown key with an empty list.

So by default dd1['any_key'] will be an empty list and the dict.append method can be applied to it.


Defaultdicts are very useful in a lot of situation where we cannot know how many keys are going to be needed.

Intermediate

Tips and tricks.


This example is a classic interview skill test.

The goal is to add a prefix to every key in a dictionary and all sub-dictionaries.

ToC

In [51]:
def modif_dict(d1, str_prefix):
    """Add a prefix to any key in a dictionary."""
    dict_tmp = {}
    for str_key in d1.keys():
        dict_tmp[str_prefix + str(str_key)] = d1.get(str_key)
        if isinstance(d1[str_key], dict):
            dict_tmp[str_prefix + str_key] = modif_dict(d1[str_key], 'clé_')

    return dict_tmp


d1 = {'A': 1, 'B': 2, 'C':
      {'C1': 3, 'C2': {'C21': 4, 'C22': 5, 'C23': {'C231': 6, 'C232': 7}}, 'C3': 8}}

d2 = modif_dict(d1, 'key_')