Container data types

Container data types#

Lists#

There are some very powerful data typs to organize larger amounts of data. Think of a situation where you are performing a measurement for a longer period of time and would like to collect the results to work on these later. One variable per measurement is clearly not the way here. But something array like would be quite helpful here. Python offers some comparable, but slightly more powerful than an array, the list.

There are two ways to create an empty list.

l = []

and

l = list()

The output looks like this.

l
[]

You can of course verify the correct type at any time using the type function.

type(l)
list

In our measurement example we will probably want to add a new measurement value, a voltage for example right after each measurement is done. The way to do this is to use the append method.

voltage = 0.1
l.append(voltage)
l
[0.1]

This can be performed over and over again until our measurements are all done

voltage = 0.2
l.append(voltage)
voltage = 0.25
l.append(voltage)
voltage = 0.11
l.append(voltage)
voltage = 0.72
l.append(voltage)
l
[0.1, 0.2, 0.25, 0.11, 0.72]

In case (some) initial values are already known, the list can as well be pre-initialized.

l = [0.1, 0.2, 0.25, 0.11, 0.72]

This will create the exact same list, but this time without the repeated append calls.

Now l holds all our volage values from the measurements to further work with. Lists support indexing. So to retrieve the 1st element we can use this syntax

l[0]
0.1

This will output the first voltage from our list. Note that Python, such as most other programming languages starts to count at zero, so in order to retrieve the first element, we need to use zero as our index value. Accessing the other values in the list is probably straightforward.

print(l[1])
print(l[2])
print(l[3])
print(l[4])
0.2
0.25
0.11
0.72

Rather surprising, but very handy, is the fact the Python supports indexing starting at the end and towards the beginning. Simply use negative index values for this.

print(l[-1])
print(l[-2])
print(l[-3])
print(l[-4])
print(l[-5])
0.72
0.11
0.25
0.2
0.1

Another interesting concept is slicing. Instead of retrieving just one element from a list, we can fetch a subset of it. The syntax for this is using colons :. E.g. start_index:end_index:every_nth denotes a slice starting at start_index up to (and excluding) end_index fetching every_nth value. Also shortcuts to this exist. Let’s have a look at some of these.

Retrieve all voltages starting with the second. start_index is 1 in this case.

l[1:]
[0.2, 0.25, 0.11, 0.72]

Fetch all values up to the third. end_index is 2 in this case.

l[:2]
[0.1, 0.2]

Fetch every second value from the beginning.

l[::2]
[0.1, 0.25, 0.72]

You can even index this way from the end. Simply use negative indices for all index positions.

l[-1:0:-2]
[0.72, 0.25]

This will give us the last value, followed by the third but not the first, as the definition is to iterate up until but excluding the end_index.

Tip

Use the dir and help methods to learn more on what a list type has to offer.

As the list type is mutable, the values in the list can be changed. You can use the same indexing approach together with an assignment to alter an existing value. To change the second voltage measured to another value, use the following.

l[1] = 1.0
l
[0.1, 1.0, 0.25, 0.11, 0.72]

Another quite remarkable feature of the list type, and that is what distinguishes it from arrays, is, that there is no need for consistency of datatypes used in the list. So overwriting an item with a text for example is perfectly fine and valid. It might not support the desired outcome of your measurement analysis workflow, but it will work. But who knows?

l[3] = "Invalid"
l
[0.1, 1.0, 0.25, 'Invalid', 0.72]

An interesting new aspect arises when adding empty lists to a new empty list. This will effectively create a two dimensional array.

data = [[], [], []]
data
[[], [], []]

You can again use the index approach to reach the inner lists. So to append values to the inner structure, you would use this.

data[0].append(1)
data[1].append(0.1)
data[2].append(1 + 1j)

data[0].append(2)
data[1].append(0.2)
data[2].append(1 + 1.1j)

data[0].append(3)
data[1].append(0.25)
data[2].append(1.1 + 1j)

data[0].append(4)
data[1].append(0.72)
data[2].append(1.2 + 0.9j)

data[0].append(5)
data[1].append(0.12)
data[2].append(0.1 + 1.2j)

data
[[1, 2, 3, 4, 5],
 [0.1, 0.2, 0.25, 0.72, 0.12],
 [(1+1j), (1+1.1j), (1.1+1j), (1.2+0.9j), (0.1+1.2j)]]

This pretty much looks like a more complex data structure to hold measurement data. The first inner list would store something like a measurement number, the second maybe represents a voltage value, whilst the last one might hold some more complex.

To retrieve a value use the indexing approach again. It’s a two step procedure now. Step into the correct inner list and fetch the value from that. The following two ways of doing this are identical, and we will use variables for the indexing.

column = 1
row = 3

dataset = data[column]
value = dataset[row]
print(value)

value = data[column][row]
print(value)
0.72
0.72

This was already quite something related to list usage. But there is more. You can use the dir command to see what more our list l has to offer.

dir(l)
['__add__',
 '__class__',
 '__class_getitem__',
 '__contains__',
 '__delattr__',
 '__delitem__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__getstate__',
 '__gt__',
 '__hash__',
 '__iadd__',
 '__imul__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__reversed__',
 '__rmul__',
 '__setattr__',
 '__setitem__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'append',
 'clear',
 'copy',
 'count',
 'extend',
 'index',
 'insert',
 'pop',
 'remove',
 'reverse',
 'sort']

There are quite a few of those dunder methods there, which implement to use some protocols to be used with the list, such as iteration for example. And we see addition, which implemented by the __add__ method. So, in a way, lists can be added. There’s also a __mul__ method, which indicates that there is also some way to perform a multiplication with lists. Interesting. The __len__ method let’s us call the len function on it. This will show the length of the list. Another interesting one is the __repr__ method. This implements a string representation of the contents of the list. Because of this we can use print to show the contents of the list. Let’s quickly try some of those.

print(l)
[0.1, 1.0, 0.25, 'Invalid', 0.72]
len(l)
5

So these where quite obvious. But what about addition?

l + ["Hallo", "Welt"]
[0.1, 1.0, 0.25, 'Invalid', 0.72, 'Hallo', 'Welt']

Addition can be used to create a new, extended version of the list. Both lists l and the new one with the items Hello and World, will be concatenated and form a new list, which can be used further on.

And how will multiplication work? Multiplication can be used to create a new list with n copies of an existing one. Here’s an example.

l * 5
[0.1,
 1.0,
 0.25,
 'Invalid',
 0.72,
 0.1,
 1.0,
 0.25,
 'Invalid',
 0.72,
 0.1,
 1.0,
 0.25,
 'Invalid',
 0.72,
 0.1,
 1.0,
 0.25,
 'Invalid',
 0.72,
 0.1,
 1.0,
 0.25,
 'Invalid',
 0.72]

The result is a list having 5 times the content of l. Not very useful, I know. This concept is more often used to pre-initialize lists with values to be replaced later on. For example to create a list of 20 zeros, just use this.

twenty_zeros = [0] * 20
print(twenty_zeros)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

But let’s now have a look at the more exposed methods of our list type. Those methods not having those double underscores. append, clear, copy, count, extend, index, insert, pop, remove, reverse, sort

We already used the append method. Use this to add single items to an existing list. With the extend method, an existing list can be extended by the contents of another list.

l.extend(["Hallo", "Welt"])

This looks pretty comparable to our addition experiment above. But this time, no new list is created, hence the missing output. This is a so called inplace operation. Printing our list l shows the new, extended state.

print(l)
[0.1, 1.0, 0.25, 'Invalid', 0.72, 'Hallo', 'Welt']

You can of course use extend to append single values as well. It’s an extension using a one elemented list. So

l.extend(["Another element"])
print(l)
[0.1, 1.0, 0.25, 'Invalid', 0.72, 'Hallo', 'Welt', 'Another element']

will extend our list by another item. But this will be kind of weird to use.

We can use copy to create a copy of our list.

l2 = l.copy()
print(l2)
[0.1, 1.0, 0.25, 'Invalid', 0.72, 'Hallo', 'Welt', 'Another element']

From now on l and l2 can be modified independently of one another. Quite useful sometimes. Note that

l3 = l[:]
print(l3)
[0.1, 1.0, 0.25, 'Invalid', 0.72, 'Hallo', 'Welt', 'Another element']

does effectively the same thing. You will probably see this version much more often than the copy method.

Copying might be an important step if you temporarily would like to modify a few items in an existing list to see where this would lead you. Notice, that a simple variable assignment to another name, probably does not have the desired effect.

l4 = l.copy()
l5 = l4
l5[1] = 1000
print(l5)
print(l4)
[0.1, 1000, 0.25, 'Invalid', 0.72, 'Hallo', 'Welt', 'Another element']
[0.1, 1000, 0.25, 'Invalid', 0.72, 'Hallo', 'Welt', 'Another element']

Changing the second element in l5, seems to have changed the value in l4 as well. Strange, isn’t it? Actually it’s not. All we have done is to create a new reference to the contents of the already existing list l4 named l5. By doing so, you can now access the same values by either using l4 or l5. And that’s why the change is visible for both l4 and l5.

But there is still more to come. count for example assists you in counting occurences of an item in a list.

l6 = [1, 2, 3, 1, 6, "Hallo"]
l6.count(1)
2

This shows, that we can find the number 1 two times in the list l6. By using index, we can find the position of an element.

l6.index(1)
0

shows, that the number 1 can be found at the first position. But that number occured twice, right? How to find the next index? The help method will be useful again here.

help(l6.index)
Help on built-in function index:

index(value, start=0, stop=9223372036854775807, /) method of builtins.list instance
    Return first index of value.
    
    Raises ValueError if the value is not present.

So the index method can have further optional arguments start and stop to narrow down the area, where value should be searched for. We can use repeated calls, omitting an already found index to find the next position.

position = l6.index(1)
print(position)
next_position = l6.index(1, position + 1)
print(next_position)
maybe_another_position = l6.index(1, next_position + 1)
print(maybe_another_position)
0
3
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[35], line 5
      3 next_position = l6.index(1, position + 1)
      4 print(next_position)
----> 5 maybe_another_position = l6.index(1, next_position + 1)
      6 print(maybe_another_position)

ValueError: 1 is not in list

The last index call resulted in a ValueError, which is correct, as we just have 2 occurences of 1 in the list.

append and extend are not the only ways to fill our list. The insert method can be used to put a new value at a certain position, moving all existing elements next to this position to the right.

l6.insert(3, "A new Value")
print(l6)
[1, 2, 3, 'A new Value', 1, 6, 'Hallo']

The next two interesting methods can be used to modify the positions of the lists contents in place. sort will sort the existing values in place, so without creating a new sorted copy. Note, that in order to sort the contents of a list, these values need to be comparable. As it makes no sense to decide if a string type will be lower or larger to a number, the attempt to sort our current list l6 will fail. So let’s quickly create a new one.

l7 = [42, 120, 0.1, -3, 12, 25]
l7.sort()
print(l7)
[-3, 0.1, 12, 25, 42, 120]

Tip

Use the sorted builtin to create a sorted copy of a list: sorted(l7)

Using reverse we can change the order of the items in the list from starting at the front to start from the back.

l7.reverse()
print(l7)
[120, 42, 25, 12, 0.1, -3]

Tip

Use the reversed builtin function to create a copy of your list in reversed order without modifying the original contend. reversed(l7)

The final methods help cleaning up.

Use remove to remove an occurance from a list.

l7.remove(25)
print(l7)
[120, 42, 12, 0.1, -3]

Using pop an item can be removed from an index position in the list returning the corresponding value. The default index is -1. So calling pop without an argument will remove an element from the back. Think of the inverse procedure of append.

removed_value = l7.pop()
print(f"The removed value is {removed_value}.")
print(l7)
The removed value is -3.
[120, 42, 12, 0.1]
removed_value = l7.pop(0)
print(f"The removed value is {removed_value}.")
print(l7)
The removed value is 120.
[42, 12, 0.1]

By using clear the list can be completely cleared.

l7.clear()
print(l7)
[]

Using clear is quite seldomly used though. Most usually to clear the list another empty one is assigned to the same variable. So the following would of course achieve the same thing.

l7 = []
print(l7)
[]

A quick return to maths#

Now that we’re able to handle lists correctly, it’s time for another builtin related to maths. You can use sum builtin to calculate the sum of values in a list. For example

values = [1, 2, 3, 4, 5]
sum(values)
15

will add up all numbers in the list to the correct result 15. This will work with floating point numbers as well. Let’s use the cool multiplication feature for lists to create a list of simple float values and add them. Say a list with ten occurences of the value 0.1. This will sum up exactly to 1.0. Easy to check. Let’s go.

values = [0.1] * 10
print(values)
sum(values)
[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
0.9999999999999999

Wait! That is not the expected result. It’s really close, but not the expected value of 1.0. Is Python that bad in calculating with floating point numbers?

No! This is a trivial example to show one of the weaknesses of floating point numbers. The precision is limited due to it’s internal design. Floating point numbers are represented using an exponential expression based on a sign, fraction and an exponent. The combination of those makes the number, and only a limited number of bits is available for each, hence the problems with precision. The main problem here though is not the internal bit representation for each value, as you can see above, as the value 0.1 is printed correctly ten times. There are problems with the floating point addition algoritmn. To solve this issue, a specialized (slower) implementation exists in the math module, which better takes care of the bit operations. Using

import math

math.fsum(values)
1.0

will provided the desired, and totally correct result.

Tuples#

Tuples are very comparable to lists, but cannot be altered. There is no way to append data to a tuple or overwrite values. In a way they can considered to be constant. I know, I’ve said the concept of constants does not really exist in Python, and a tuple is very much this. However, from the practical perspective tuples wouldn’t be very convenient to use when it comes to handle a single constant value. And putting all those values considered to be constant into a single tuple and using the via indices … Yeah, that’s not very practical either. Named Tuples though …., but that’s a different, more advanced story. Let’s focus on the simple version here. So tuples are very comparable to lists. Instead of square brackets use the round ones to create a tuple or use the tuple keyword.

t = ()
t = tuple()

Let’s quickly check if we really created a typle data type.

type(t)
tuple

Yes, that worked okay. So, what has a tuple to offer? We can use the dir function again to query for the tuples methods.

dir(tuple)
['__add__',
 '__class__',
 '__class_getitem__',
 '__contains__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__getnewargs__',
 '__getstate__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__rmul__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'count',
 'index']

There are quite some dunder methods again. A few we already recognize. The __len__ is there, so we can get the size of the tuple using the len function. An __add__ and __mul__ method is available. So in some way we could add or multiply those tuples. The only exposed methods are count and index. And they will do just the same thing as for lists. count will support you in getting the number of occurences of an item in the tuple, whereas index will show you the position of an element in the tuple. So seeing a tuple as a readonly and therefore reduced version of a list does make sense. Let’s play around with these tuples a bit.

t1 = (1, "Hello", 2)
t2 = (12, 24, 42)
t3 = t1 + t2
print(t3)
type(t3)
(1, 'Hello', 2, 12, 24, 42)
tuple

So adding to tuples joins the tuple in a newly created again readonly tuple.

t4 = t1 * 5
print(t4)
type(t4)
(1, 'Hello', 2, 1, 'Hello', 2, 1, 'Hello', 2, 1, 'Hello', 2, 1, 'Hello', 2)
tuple

Whereas multiplication repeats the tuple a number of times to create a new tuple.

Tuples and lists can be converted into each other by using the one as an argument in the others construction.

l1 = list(t1)
print(l1)
type(l1)
[1, 'Hello', 2]
list

This created the list l1 out of our tuple t1, whereas

t1 = tuple(l1)
print(t1)
type(t1)
(1, 'Hello', 2)
tuple

will create a tuple out of our newly created list, assigning it to the varable name t1, hence overwriting the original t1 contents. So tuples cannot be modified, but of course completely replaced.

And here’s something interesting. If I can put anything into a tuple, what about creating a tuple of tuples then.

t5 = ((), (), ())
print(t5)
type(t5)
((), (), ())
tuple

This went perfectly fine. However it’s not very useful. The contents of the tuple cannot be modified.

t5[0]
()
t5[0] = 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[59], line 1
----> 1 t5[0] = 1

TypeError: 'tuple' object does not support item assignment

Indexing to the inner tuples works as expected, but the contents there, being a tuple, cannot be modified either. So it will just stay a tuple having three empty tuples. But what about adding lists to the the tuple?

t6 = ([], [], [])
print(t6)
type(t6)
([], [], [])
tuple

Now we have a tuple with 3 empty lists. Let’s see if we can add contents to the lists.

t6[0].append(1)
t6[1].append(2)
t6[2].append(3)
print(t6)
type(t6)
([1], [2], [3])
tuple

This worked, and was expected of course. Whilst the tuple is immutable and because of this there is no chance to increase or decrease the number of elements in the tuple itself, the inner lists are happy to handle content. Cleaning up here will be a bit more difficult, as you cannot overwrite the contents of the tuple with empty lists. Yeah, that readonly feature again. But you can of course remove the items from the list using pop, or simply use the clear method.

t6[0].pop()
t6[1].clear()
t6[2].pop()
print(t6)
([], [], [])

Sets#

Sets are modeled after the mathematical concept of sets. It’s basic feature is, that elements can exist only once in a set. You can create an empty set like this.

s = set()
type(s)
set

A quick dir on the newly creates set shows what this data type has to offer.

dir(s)
['__and__',
 '__class__',
 '__class_getitem__',
 '__contains__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getstate__',
 '__gt__',
 '__hash__',
 '__iand__',
 '__init__',
 '__init_subclass__',
 '__ior__',
 '__isub__',
 '__iter__',
 '__ixor__',
 '__le__',
 '__len__',
 '__lt__',
 '__ne__',
 '__new__',
 '__or__',
 '__rand__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__ror__',
 '__rsub__',
 '__rxor__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__sub__',
 '__subclasshook__',
 '__xor__',
 'add',
 'clear',
 'copy',
 'difference',
 'difference_update',
 'discard',
 'intersection',
 'intersection_update',
 'isdisjoint',
 'issubset',
 'issuperset',
 'pop',
 'remove',
 'symmetric_difference',
 'symmetric_difference_update',
 'union',
 'update']

This again is quite a set of functionality available. Based on the fact that the Python set represents the mathmatical idea of sets, we expect a few things to work. So let’s have a closer look. First of all, we expect to be able to add things to our set.

s.add(1)
s
{1}

Okay. So this is the way we add items to our set. Let’s add some more.

s.add(42)
s.add(50)
s.add(2.1)
s.add(17)
s
{1, 2.1, 17, 42, 50}

Fine. What about adding items that already exist?

s.add(42)
s
{1, 2.1, 17, 42, 50}

This also works as expected. No error, but as the item 42 is already in the set, there is no second occurance.

For removing items we also have an idea. pop and remove exist here, so they probably work as well as expected.

removed_item = s.pop()
print(removed_item)
s.remove(17)
s
1
{2.1, 42, 50}

Note that remove deletes the item from the set without returning it, which makes sense as you probably already know what you’re removing.

Also calling len on the set is possible, as the __len__ method exists.

len(s)
3

But we expect some more specialized methods when working with sets, such as joining two sets, building the difference, an intersection, or checking if one set is part (subset) of another or a superset. All of these expected methods exist, and usually a well known builtin operator can be used instead as well, as it will support a very readable syntax. Consider the following two sets having interger values.

s1 = {1, 2, 3, 4, 5}
s2 = {4, 5, 6, 7, 8}

We can union these two sets by either of the two following expressions.

s1.union(s2)
{1, 2, 3, 4, 5, 6, 7, 8}

When creating a union of two sets, the idea is to have a group of elements, which are either in s1 or in s2. So the element wise or operation | will do the job here just the same.

s1 | s2
{1, 2, 3, 4, 5, 6, 7, 8}

Building the difference between the two sets goes like this. We like to get all elements in s1 that are not in s2 or vice versa.

s1.difference(s2)
{1, 2, 3}
s2.difference(s1)
{6, 7, 8}

The subtraction operator - will provide just the same result.

s1 - s2
{1, 2, 3}
s2 - s1
{6, 7, 8}

What about the intersection?

s1.intersection(s2)
{4, 5}

We’re looking for elements, that are both in s1 and s2. So the element wise and operation & will be equivalent here.

s1 & s2
{4, 5}

We know one more element wise operator from our work with boolean operations with number values. The exclusive or operation xor, available using the ^ operator. We can fetch all values being exclusively either in s1 or in s2 with that.

s1 ^ s2
{1, 2, 3, 6, 7, 8}

The exposed method implementing this is the symmetric_difference function.

s1.symmetric_difference(s2)
{1, 2, 3, 6, 7, 8}

Whats finally missing is the subset handling. Let’s create a new test set for this.

s3 = {2, 3}

We can use the < operator or the issubset method to check if s3 is a subset of s1

s3 < s1
True
s3.issubset(s1)
True

If we read this the other way around, we’ll perform a superset check. Use the > operator or issuperset method for this.

s1 > s3
True
s1.issuperset(s3)
True

For the sake of completeness there is two more operators to be used here, namely the >= and <= operators. They are very similar to the > and < operators in that they check for superset or subset properties, however this time they decide on of a set is a proper sub or superset. A very subtle difference, where a set can be a subset of another set, if both sets have the exact same values in it. In that case however, the set is not a proper subset. Same is true the other way around for supersets.

s3 >= s1
False
s3 <= s1
True

I think this should be enough for handling sets. Note that the most usual syntax for dealing with sets is using operators such as -, <, | and so forth instead of their exposed methods, as this is pretty much forward to read. Please note as well, that all those operators can be chained and will be evaluated from left to right in that case. We can create some remarkable complexity with this. Those one liners might feel very powerful, but quite often they’re not that easy to follow. Take care of that in your work.

Tip

Play around with the other exposed methods for sets and try some chaining for set operations to get the feel for these operations.

Dictionaries#

Dictionaries are possibly the most powerful data type in Python. A dictionary is a key-value store, mapping values to a names. That means, that you can save some arbitrary value by assigning it to a label for later use. You can either assign or retrieve values that way. There is just one restriction to chosing the data type for the label. It has to be hashable. Pretty much the same as for adding values to sets.

A dictionary can be created in the follwing two ways.

d = {}
d = dict()
type(d)
dict

As usual dictionaries can also be pre filled. The syntax is as follows. It will assign to key-value pairs to the dict, maybe related to some time based measurement that has been defined.

d = {
    "time": 0,
    "value": 0
}
print(d)
{'time': 0, 'value': 0}

Calling dir on the dict will show us the dicts features we can use.

dir(d)
['__class__',
 '__class_getitem__',
 '__contains__',
 '__delattr__',
 '__delitem__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__getstate__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__ior__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__ne__',
 '__new__',
 '__or__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__reversed__',
 '__ror__',
 '__setattr__',
 '__setitem__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'clear',
 'copy',
 'fromkeys',
 'get',
 'items',
 'keys',
 'pop',
 'popitem',
 'setdefault',
 'update',
 'values']

Quite some well known friends among them. There’s the __len__ dunder, so calling len on the dict will probably work. There is __repr__, and we’ve seen its effect above already when printing the dict d. copy will probably perform the expected action, we can clean up using clear and we can remove items by poping them out of the dict. The new kids in town are keys, values, items, as well as get, popitem and update here. Let’s have a closer look.

The keys method will let us fetch all keys available in the dictionary.

keys = d.keys()
keys
dict_keys(['time', 'value'])

In contrast to that the values method retrieves all values stored along with the keys.

values = d.values()
values
dict_values([0, 0])

Note

Please note that the return type of keys and list is not of type list. So it does not support indexing. Shall you want to use it this way, you can quickly make a list out of it using list(d.keys()) for example.

The dictionary type also supports to get all key-value pairs at once. Use the items method for this.

d.items()
dict_items([('time', 0), ('value', 0)])

The value for a key can be retrieved using get. However, this is seldomly used. The much preferred variant of this is to use the squared brackets syntax, just as for lists, tuples and sets, but this time with the key instead of an index number.

print(d.get("time"))
print(d["time"])
0
0

A way to remove an entry from a dict is to use the pop method. It will return the value to the key specified and remove the complete entry.

value = d.pop("time")
value
0

The contents of the dict is now this.

d
{'value': 0}

Using popitem you can remove the last entry from the dict in the same way it works for lists for example. It will return a key-value pair.

d.popitem()
('value', 0)

Effectively out dict is now empty.

d
{}

The last method of interest now is the update method. Using update an existing dict can be updated using another dict as its argument. Existing values will be overwritten by this call, as it’s an inplace operation.

d.update({"time": 0, "value": 0.1})
d
{'time': 0, 'value': 0.1}

In newer versions of Python the same can be achieved using the element wise or operator |. This works the same way as for sets.

d  | {"time1": 1, "value1": .2}
{'time': 0, 'value': 0.1, 'time1': 1, 'value1': 0.2}

In the same way as the other container types also dicts can become multidimensional. As soon as you start to put another dict as a value to an existing dict, effectively the dimensionality of the original dict increased. This is usually used to create hierarchical data models. Consider you plan to create a birthday calendar. One approach for this could be store months first level, and the day in the second level. The value behind that would be the name of that person. It might look like this in that case.

birthdays = {
    "january": {
        1: "Susie",
        15: "Joe",
    },
    "march": {
        23: "Jane",
    },
    "june": {
        15: "Fred",
        22: "Gilbert",
    },
}
print(birthdays)
{'january': {1: 'Susie', 15: 'Joe'}, 'march': {23: 'Jane'}, 'june': {15: 'Fred', 22: 'Gilbert'}}

To retrieve all birthday entries in january, you would access the key “january” in the birthdays dict. The return value will be another dict.

birthdays["january"]
{1: 'Susie', 15: 'Joe'}

And to see the name of the person celebrating birthday on january 15th, use a two-level approach like so.

birthdays["january"][15]
'Joe'

Quite powerful, isn’t it? Using dicts is very common in Python. It is probably the most used data type for managing data, because of its flexibility and ease of handling. I strongly suggest to play around with using dicts to gain more experience in using them,