In the previous post, we tackled stuff that has accompanied us since the very first post, that are functions. 👇👇👇

As we already know, many of them are built in Python. But that's not everything, it's just the tip of the iceberg. Now let's find out how to store a collection of elements, or map one element to the other. It is time to dive into *tuples*, *lists*, *sets*, and *dictionaries*.

# Tuples

A tuple is an immutable ordered collection of elements, enclosed in parentheses. Once created, we cannot modify its elements. Tuples are commonly used to group related data together.

As a matter of fact, tuples are something we are already familiar with. Let's dwell on the example from the previous post.

```
def add_and_subtract_numbers(a, b):
sum_result = a + b
diff_result = a - b
return sum_result, diff_result
sum_result, diff_result = add_and_subtract_numbers(10, 5)
print("Sum:", sum_result)
print("Difference:", diff_result)
```

In the above example, we have the function that computes two values, outcomes of adding and subtracting input parameters.

```
def add_and_subtract_numbers(a, b):
sum_result = a + b
diff_result = a - b
return sum_result, diff_result
outcome = add_and_subtract_numbers(10, 5)
print(type(outcome))
```

Now instead of unpacking returned values, we directly print the type of the returned value.

```
❯ python3 main.py
<class 'tuple'>
```

The class of returned value is a **tuple**!

To define a tuple we can either return multiple values from a function or use round parenthesis `( )`

.

```
>>> x = (1, 'a', 1.1)
>>> print(type(x))
<class 'tuple'>
```

The one way of extracting values from a tuple is unpacking it.

```
>>> a, b, c = x
>>> print(a)
1
>>> print(b)
a
>>> print(c)
1.1
```

As already has been said, if can discard values we are not interested in, using the underscore `_`

.

```
>>> _, _, d = x
>>> print(d)
1.1
```

When a tuple contains many values, we can unpack it partially using an asterisk `*`

.

```
>>> x = (1, 'a', 2, 'b', 3, 'c')
>>> a, *y, b = x
>>> print(a)
1
>>> print(y)
['a', 2, 'b', 3]
>>> print(b)
c
>>> type(y)
<class 'list'>
```

We can easily notice that the first element has been unpacked to `a`

and the last one to `b`

, all the rest of the elements have been unpacked to `y`

as a *list*. If we are interested in the first and the last elements only, we can simply discard the other ones using the underscore `_`

.

`>>> a, *_, b = x`

Another way of extracting values from a tuple is to use square parenthesis `[]`

. This method can be used for slicing a tuple, reversing it, or obtaining concrete elements.

```
>>> x = (1, 'a', 2, 'b', 3, 'c')
>>> x[2:6]
(2, 'b', 3, 'c')
>>> x[::-1]
('c', 3, 'b', 2, 'a', 1)
>>> x[1]
'a'
>>> x[-1]
'c'
```

In the above example, we have created another tuple that contains a subset of elements from the original one, we have reversed it and obtained the first and the last element. Please note when slicing or reversing a tuple, the original one remains intact.

We can concatenate tuples using the `+`

operator.

```
>>> tuple1 = (1, 2, 3)
>>> tuple2 = (4, 5, 6)
>>> tuple1 + tuple2
(1, 2, 3, 4, 5, 6)
```

We can repeat elements using the `*`

operator.

```
>>> tuple1 * 3
(1, 2, 3, 1, 2, 3, 1, 2, 3)
```

To check the length of a tuple, we can use the *len()* function. We can also check if an element exists in a tuple using the membership operator.

```
>>> colors = ("red", "green", "blue")
>>> len(colors)
3
>>> "red" in colors
True
```

To convert a tuple to a list and back we can simply use the *list()* and the *tuple()* functions respectively.

```
>>> my_tuple = (10, 20, 30)
>>> tuple_as_list = list(my_tuple)
>>> print(tuple_as_list)
[10, 20, 30]
>>> tuple_as_list.append(40)
>>> modified_tuple = tuple(tuple_as_list)
>>> print(modified_tuple)
(10, 20, 30, 40)
```

💡 Remember, since tuples are immutable, direct modifications like adding or removing elements are not possible.

Tuples are useful when we have data that should not be changed after creation, like coordinates, constant configurations, or when returning multiple values from a function. Their immutability ensures data integrity and can help prevent unintended modifications.

# Lists

A list is a mutable ordered collection of elements, enclosed in square brackets. Compared to tuples, lists can be modified after creation by adding, removing, or changing elements.

To create a list, we can use square parenthesis `[]`

.

```
>>> aList = []
>>> print(type(aList))
<class 'list'>
>>> print(aList)
[]
```

Here we have created an empty list. We can create one with prepopulated data. To do that, we simply have to provide elements in brackets.

```
>>> aList = ['a', 2, 3.3]
>>> print(aList)
['a', 2, 3.3]
```

We have successfully created a list with three elements: a number, a string, and a float. To access elements we can simply use square brackets `[]`

.

```
>>> first = aList[0]
>>> second = aList[1]
>>> last = aList[-1]
>>> print(first, second, last)
a 2 3.3
```

In contrast to tuples, lists are mutable. It means we can easily modify, add, or remove elements during execution.

```
>>> aList[0] = 1
>>> print(aList)
[1, 2, 3.3]
>>> aList.append(6)
>>> print(aList)
[1, 2, 3.3, 6]
>>> aList.insert(1, 9)
>>> print(aList)
[1, 9, 2, 3.3, 6]
>>> aList.remove(1)
>>> print(aList)
[9, 2, 3.3, 6]
```

We have overridden the zeroth element to 1. Then we have added 6 to the list. Then 9 has been inserted at position 1. Finally, we have removed element by value.

To remove elements by an index, the *pop()* method can be used.

```
>>> aList.pop(2)
3.3
>>> print(aList)
[9, 2, 6]
```

The float has been removed from the list.

💡 While a function is a block of reusable code that performs a specific task, a method is a function that

belongs to an object or a class.

We can repeat elements using the `*`

operator.

```
>>> aList = aList * 3
>>> print(aList)
[9, 2, 6, 9, 2, 6, 9, 2, 6]
```

To create sublists we can use square brackets `[]`

.

```
>>> aList[2:7]
[6, 9, 2, 6, 9]
```

To create a copy of the original list we use square brackets with a colon `[:]`

.

```
>>> aSecondList = aList[:]
>>> print(aSecondList)
[9, 2, 6, 9, 2, 6, 9, 2, 6]
>>> id(aList)
4681078144
>>> id(aSecondList)
4682931200
```

To obtain a sorted list we can use the *sorted()* function.

```
>>> sorted(aList)
[2, 2, 2, 6, 6, 6, 9, 9, 9]
```

That function creates another, sorted list from the original one. To sort in-place, we can call the *sort()* method.

```
>>> aList.sort()
>>> print(aList)
[2, 2, 2, 6, 6, 6, 9, 9, 9]
```

To reverse a list, we can use either the *reverse()* method or the *reversed()* function.

```
>>> reversed(aList)
<list_reverseiterator object at 0x11614da20>
>>> list(reversed(aList))
[9, 9, 9, 6, 6, 6, 2, 2, 2]
>>> aList.reverse()
>>> print(aList)
[9, 9, 9, 6, 6, 6, 2, 2, 2]
```

The *reversed()* function actually creates an *iterator*. To obtain a new list, we need to pass the iterator into the *list()* function.

💡 An

iteratoris an object that enables the traversal of a collection of elements, one at a time, without needing to know the specific details of the underlying structure of the collection. Iterators provide a uniform way to access and process elements in sequences, containers, or other iterable objects.

There are even more useful methods such as:

`list.count()`

method provides the ability to count occurrences of an element,`list.index()`

method provides the ability to find an index of an element,`len(list)`

function provides a way to find the length of the list,`list.clear()`

clears all elements from the list.

Lists are incredibly versatile and widely used for managing sequences of items. They offer flexibility through methods for adding, removing, and modifying elements, making them a fundamental tool in programming.

# Sets

A set is an unordered collection of unique elements, enclosed in curly braces or created using the *set()* function. Sets are useful when we need to ensure uniqueness or perform set operations like union, intersection, etc.

To create an empty set we use the *set()* function, and to create a set with prepopulated data, we use curly braces.

```
>>> anEmptySet = set()
>>> print(type(anEmptySet))
<class 'set'>
>>> print(anEmptySet)
set()
>>> aSet = {9, 2, 7, 1, 3}
>>> print(type(aSet))
<class 'set'>
>>> print(aSet)
{1, 2, 3, 7, 9}
```

We can see that even if we have entered the numbers randomly, the set keeps them in order. To add a new element to the set, we simply call *add()* method.

```
>>> aSet.add(13)
>>> print(aSet)
{1, 2, 3, 7, 9, 13}
```

Number 13 has been added to the set. Now to remove an element, we can use either the *remove()* method or the *discard()* method.

```
>>> print(aSet)
{1, 2, 3, 7, 9, 13}
>>> aSet.remove(1)
>>> print(aSet)
{2, 3, 7, 9, 13}
>>> aSet.discard(7)
>>> print(aSet)
{2, 3, 9, 13}
```

We have removed 1 and 7 from the set. The major difference between those two methods is *remove()* method throws an exception if an element is not in the set.

```
>>> print(aSet)
{2, 3, 9, 13}
>>> aSet.discard(1)
>>> aSet.remove(1)
Traceback (most recent call last):
File "<string>", line 1, in <module>
KeyError: 1
```

We have tried to remove 1 from the set. In the case of the *remove()* method, a *KeyError* exception was thrown.

💡 An

exceptionis an event that occurs during the execution of a program, which disrupts the normal flow of the program’s instructions. Exceptions are raised when an error or unexpected condition occurs, and they allow the program to handle these exceptional situations gracefully, rather than crashing.

Sets support a variety of operations, like intersection, union, or difference. Union combines two sets, returning a new set containing all unique elements from both sets.

```
>>> set1 = {1, 2, 3}
>>> set2 = {3, 4, 5}
>>> set1.union(set2)
{1, 2, 3, 4, 5}
```

Here we have created a union containing numbers 1, 2, 3, 4, and 5, from the `set1`

and the `set2`

. Difference returns a new set containing elements that are in the first set but not in the second set.

```
>>> set1 = {1, 2, 3}
>>> set2 = {3, 4, 5}
>>> set1.difference(set2)
{1, 2}
```

Computing the difference between the `set1`

and the `set2`

has created a set containing numbers 1 and 2. Intersection returns a new set containing elements that are present in both sets.

```
>>> set1 = {1, 2, 3}
>>> set2 = {3, 4, 5}
>>> set1.intersection(set2)
{3}
```

The intersection has created a set containing a single number 3.

We can easily find the number of elements in a set using the *len()* function.

```
>>> set1 = {1, 2, 3}
>>> set2 = {3, 4, 5}
>>> len(set1)
3
```

To find out whether an element is included in the set, we can use membership testing.

```
>>> set1 = {1, 2, 3}
>>> 1 in set1
True
>>> 5 in set1
False
```

Sets are particularly useful when we want to work with unique elements and perform set-related operations. They are designed for efficient membership tests and are helpful when we need to ensure that elements are unique within a collection.

# Frozensets

A frozenset is an immutable version of a set. It is essentially a set that cannot be modified after it is created. This means we cannot add, remove, or change elements in a frozenset. Frozenset is created using the *frozenset()* function.

`>>> aFrozenSet = frozenset([1, 2, 3, 4])`

Frozensets are useful in scenarios where we need to ensure that a set of elements remains constant and should not be modified. They are commonly used as keys in dictionaries when we want to create dictionaries with keys consisting of multiple values.

# Dictionaries

A dictionary is a versatile and unordered collection of data in the form of key-value pairs. Each key is unique and is used to access its corresponding value. Dictionaries are also known as associative arrays or hash maps in other programming languages. They are created using curly braces `{}`

or the *dict()* constructor.

Let’s create a simple dictionary that maps names to ages. A single key-value pair is provided in the form of `'key': value`

and separated from the others by `,`

. Everything is embraced by curly braces `{}`

.

```
>>> aDict = { 'Adam': 21, 'Paul': 32, 'John': 43 }
>>> print(aDict)
{'Adam': 21, 'Paul': 32, 'John': 43}
```

We have created a dictionary containing three mappings - three names mapped to corresponding ages.

We can look up values by providing a key in square braces - in our particular example - names.

```
>>> print(aDict)
{'Adam': 21, 'Paul': 32, 'John': 43}
>>> aDict['Paul']
32
```

Qyering the dictionary for *"Paul"* results in obtaining the *number 32*. To update a mapping, we assign a new value to the existing key.

```
>>> print(aDict)
{'Adam': 21, 'Paul': 32, 'John': 43}
>>> aDict['Paul'] = 54
>>> print(aDict)
{'Adam': 21, 'Paul': 54, 'John': 43}
```

Now mapping for *"Paul"* has been updated to number 54. To insert a new mapping into the dictionary, we assign a new value to the non-existing key.

```
>>> print(aDict)
{'Adam': 21, 'Paul': 54, 'John': 43}
>>> aDict['Mat'] = 66
>>> print(aDict)
{'Adam': 21, 'Paul': 54, 'John': 43, 'Mat': 66}
```

A new key-value pair, *"Mat": 66,* has been added to the dictionary.

Lastly, we can remove elements from a dictionary using the `del`

keyword.

```
>>> print(aDict)
{'Adam': 21, 'Paul': 54, 'John': 43, 'Mat': 66}
>>> del aDict['Adam']
>>> print(aDict)
{'Paul': 54, 'John': 43, 'Mat': 66}
```

In the above example, *"Adam"* has been removed from the dictionary.

Overall, dictionaries are a fundamental and powerful data structure in Python, widely used in various applications due to their flexibility and efficiency.

# Summary

In this post, we explored data structures like tuples, lists, sets and dictionaries.** Tuples** are ordered, immutable collections of elements and are useful for grouping related data. **Lists** are ordered, mutable collections of elements. They allow for dynamic changes such as adding, removing, and modifying elements. **Sets** are unordered collections of unique elements. They ensure the uniqueness of elements and support operations like intersection, union, and difference. **Dictionaries** are unordered collections of key-value pairs. They enable efficient retrieval of values based on unique keys. Dictionaries are used to represent mappings and associations between data elements.