5.1. Readings: lists#

In Chapter 3, we first faced the need of encapsulating many values in the same variable when introducing the fermented_food and fermented_drinks variables that had a collection of values. Both variables were examples of lists.

5.1.1. Lists#

The syntax for declaring a list is:

list_name = [item1, item2, item3,...]

Anything enclosed in square brackets [] and separated by , will be a list. With the above piece of code you create a list and assign it to the list_name variable. Some examples of lists would be:

natural_numbers = [1, 2, 3, 4, 5]
rational_numbers = [1.1, 2.5, 3.4]
letters = ['p', 'y', 't', 'h', 'o', 'n']
booleans = [True, False, False, False, True]

The lists in the examples above contain the same data types, which means that they are homogenous. However, in Python it is possible/allowed for a list to contain different data types. These lists are called heterogenous. For example:

mixed_list = [1, 'p', 1.1, True]

An in previous chapters, we can use the print() function to print the contents of a list:

print(natural_numbers)
[1, 2, 3, 4, 5]

5.1.1.1. Number of elements in a list#

In most cases we do not know how many elements there are in a list. It is possible that during the execution of the program the number of elements in a list will change, e.g., if we add/remove elements to/from a list, accept a list created from user inputs, etc. In these situations, if we would like to iterate over the elements of a list, it would be impossible to hard-code the number of iterations like we did so far in the previous chapters. That’s why the len() function is very useful. It gets a list as an argument and outputs the length of that list, in other words: the number of elements that the list has. For example:

len(mixed_list)
4

5.1.1.2. Accessing elements of the list#

Lists in Python use 0-based indexing. This means that the first element is at position 0, the second element at position 1 and so on. Fig. 5.1 illustrates this idea.

indexing_illustration

Fig. 5.1 Indexing in Python#

Hint

Since Python uses 0-based indexing this means that in a list with n elements, the last element will be in position n-1.

In order to access element t in the letters list, in the example above we would write:

letters[2]
't'

Although character t is the third element in the list, in order to access it we should use index 2 since lists use 0-based indices.

In Python we can also access elements of a list using negative indices. In this setting, the last element of the list is at position -1, the second from the last is at position -2 and so on. Fig. 5.2 illustrates the idea:

neg_indexing_illustration

Fig. 5.2 Negative Indexing in Python#

letters[-4] #accessing element 't'
't'

Caution

If we try to access an element at a position that does not exist then we will get an IndexError. E.g: in the letters list above if we try to access elements at position 9 or -8 then we will get an IndexError since the letters list does not contain 10 or 8 elements respectively.

letters[9]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[7], line 1
----> 1 letters[9]

IndexError: list index out of range

We can use indices in a loop to access elements of a list as well, just as we did in Nested Loops in Chapter 3. For example, to print each element of the letters list using a for loop we would do:

for i in range(0, len(letters)):
    print(letters[i], end=' ')
p y t h o n 

Similarly, using a while loop we could write:

j = 0
while j < len(letters):
    print(letters[j], end=' ')
    j+=1
p y t h o n 

Important

Here we used the function len(letters) to determine the number of elements of the letters list inside the range() function. So the range() function will produce the sequence of numbers {0, 1, 2,..., len(letters)-1} that corresponds to the indices needed to access the elements of the list. Thus we do not need to hardcode the number of elements that a list will have since we can access it via the len() function, as explained in the previous section.

We can also select a range of values in a list by placing a range inside the square brackets with the following syntax:

list_name[start_pos:end_pos]

where end_pos is non-inclusive. This is called slicing.

letters[1:4] # print letters of positions 1,2,3
['y', 't', 'h']

There are also variations of range selection in lists. For example, if we write:

  • list_name[start_pos: ] - this will select all elements from start_pos until the end of the list.

  • list_name[ :end_pos] - this will select all elements from the beginning until the end_pos non-inclusive.

  • list_name[ : ] - this will select all elements of the list (equivalent to calling list_name).

5.1.1.3. Adding/removing elements to/from a list#

Lists are mutable data-types. This means that we can modify a list and its objects by adding, deleting, or modifying certain list elements.

5.1.1.3.1. Modifying elements of a list#

In order to modify a list element we access it using its index and then we set it to the new value. For example, suppose we want to change the second element of the natural_numbers list from 2 to 10. Then we would write:

print('List of natural numbers before changes:', natural_numbers)
natural_numbers[1] = 10
print('List of natural numbers after changes:', natural_numbers)
List of natural numbers before changes: [1, 2, 3, 4, 5]
List of natural numbers after changes: [1, 10, 3, 4, 5]

5.1.1.3.2. Adding elements to a list#

In order to add elements to a list we can use the += operator. We can add a single value or many values in the form of iterable (list, tuple, etc).

natural_numbers += [20]
print(natural_numbers)
natural_numbers += [30, 40]
print(natural_numbers)
[1, 10, 3, 4, 5, 20]
[1, 10, 3, 4, 5, 20, 30, 40]

Caution

Be careful if you try to add a single value to a list using the += operator. It will raise a TypeError since a single value is not an iterable (collection of values).

natural_numbers += 20
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 1
----> 1 natural_numbers += 20

TypeError: 'int' object is not iterable

There are other ways to add elements to a list, using different built-in methods.

We can use the append() method to add a single element to a list.

rational_numbers.append(4.7)
print(rational_numbers)
[1.1, 2.5, 3.4, 4.7]

We can use the insert() method to add an element to a list at a specific position. Its syntax is:

list_name.insert(position, element)
print('natural_numbers before adding 50:', natural_numbers)
natural_numbers.insert(3, 50)
print('natural_numbers after adding 50:', natural_numbers)
natural_numbers before adding 50: [1, 10, 3, 4, 5, 20, 30, 40]
natural_numbers after adding 50: [1, 10, 3, 50, 4, 5, 20, 30, 40]

In order to add elements of any type of collection (tuples, sets and dictionaries) to a list we can use the extend() method as well. The syntax is:

list_name.extend(collection_to_be_added)

For example, we can append the natural_numbers modified list to the rational_numbers list using the extend() method.

print('rational_numbers before extending to natural_numbers:', rational_numbers)
rational_numbers.extend(natural_numbers)
print('rational_numbers after extending to natural_numbers:', rational_numbers)
rational_numbers before extending to natural_numbers: [1.1, 2.5, 3.4]
rational_numbers after extending to natural_numbers: [1.1, 2.5, 3.4, 1, 10, 3, 50, 4, 5, 20, 30, 40]

5.1.1.3.3. Removing elements from a list#

There are two methods to remove elements from a list: remove() and pop(). The remove() method allows us to specify an element from the list to remove.

print('natural_numbers before removing 50:', natural_numbers)
natural_numbers.remove(50)
print('natural_numbers after removing 50:', natural_numbers)
natural_numbers before removing 50: [1, 10, 3, 50, 4, 5, 20, 30, 40]
natural_numbers after removing 50: [1, 10, 3, 4, 5, 20, 30, 40]

The pop() method has two versions: one without any arguments and one with one argument. If we call pop() without any arguments then it will remove the last element from the list. If we call pop(index) it will remove the item at position index.

print('natural_numbers before removing last element:', natural_numbers)
natural_numbers.pop()
print('natural_numbers after removing last element:', natural_numbers)
natural_numbers before removing last element: [1, 10, 3, 4, 5, 20, 30, 40]
natural_numbers after removing last element: [1, 10, 3, 4, 5, 20, 30]
print('natural_numbers before removing element at position 4:', natural_numbers)
fourth_item = natural_numbers.pop(4)
print('natural_numbers after removing element at position 4:', natural_numbers)
print('we removed this item from the list:', fourth_item)
natural_numbers before removing element at position 4: [1, 10, 3, 4, 5, 20, 30]
natural_numbers after removing element at position 4: [1, 10, 3, 4, 20, 30]
we removed this item from the list: 5

5.1.1.4. List comprehensions#

Another way to iterate over a list is through list comprehensions. The syntax for list comprehensions is:

new_list = [do something on item x for x in iterable_name (if condition==True)]

The condition is not mandatory to be included. Below we will see examples that include a condition part and examples that do not include a condition part. Also, the iterable_name can be any iterable (list, tuple, set, dictionary). x is the variable used to iterate the list, which will represent one element of the iterable per iteration, will be modified by the do something on item x part, and will be appended to the new_list.

As you can see, list comprehensions provide a shorthand syntax for creating a list by modifying or filtering elements of the current list, in only one line of code.

For example, to iterate over the elements of the letters list we would write:

[print(letter, end=' ') for letter in letters];
p y t h o n 

To illustrate the usefulness of list comprehension, take for example the following for loop:

list_of_numbers = []
for i in range(0, 5, 2):
    list_of_numbers.append(i**2)
list_of_numbers
[0, 4, 16]

The above loop creates a list of integers from the squares of the given range. We can do the same in one line of code using list comprehension:

[x**2 for x in range(0, 5, 2)]
[0, 4, 16]

Now suppose that we want to split the elements of the letters list into vowels and consonants. The vowels in the English language are: a, e, i, o, u, y, so we will have them in the vowels list. We will have all the vowels from the letters list in one list and all the consonants on another. For this we can use a list comprehension.

vowels = ['a', 'e', 'i', 'o', 'u', 'y']

vowels_in_letters = [letter for letter in letters if letter in vowels]
consonants_in_letters = [letter for letter in letters if letter not in vowels]

print('Vowels in letters:', vowels_in_letters)
print('Consonants in letters:', consonants_in_letters)
Vowels in letters: ['y', 'o']
Consonants in letters: ['p', 't', 'h', 'n']

There is a possibility to include more than one condition. In this case the syntax of the list comprehension changes a bit because the condition would come before the for-loop. The syntax would be:

new_list = [a if condition=True else a=b  for a in iterable_name]

For example, the example above of separating the vowels from consonants would be written as:

vowels_list = []
consonants_list = []

[vowels_list.append(x) if x in vowels else consonants_list.append(x) for x in letters]

print('Vowels in letters:', vowels_in_letters)
print('Consonants in letters:', consonants_in_letters)
Vowels in letters: ['y', 'o']
Consonants in letters: ['p', 't', 'h', 'n']

Thus, using two conditions (if/else) in the statement above is equivalent to the two list-comprehensions in the previous example. In other words, this statement single-handedly does the job. Let us explain what it does. We are iterating over the letters list using the variable x. For each x, we check if it is in the vowels list defined in the previous code cell. If this condition is True, then we append the letter x to the vowels_list, otherwise we append x to the consonants_list. So the first statement vowels_list.append(x) is executed only if the condition in the if part is True, otherwise the statement in the else part is executed. Fig. 5.3 illustrates this case too.

list_comp_illustration

Fig. 5.3 List comprehension with two conditions#

5.1.1.5. Sorting lists#

When programming, quite often we face the need to sort inputs (inside sequences) according to some criteria. In Python there is a built-in function sorted(), that can receive a list as an argument and sort it. Also there is the built-in method list_name.sort() that is called on a list object and by default sorts a list in ascending order.

Important

The built-in function sorted(), returns a new list and the passed list remains unchanged. The built-in list_name.sort() method does the sorting in-place. This means that the original list will be modified.

For example:

letters.sort()
print('letters in ascending order:', letters)

letters.sort(reverse=True)
print('letters in descending order:', letters)
letters in ascending order: ['h', 'n', 'o', 'p', 't', 'y']
letters in descending order: ['y', 't', 'p', 'o', 'n', 'h']

As you can see, the sort() method modifies the original list, letters.

Sorting using the sorted() built-in function:

sorted_numbers = sorted(natural_numbers)
print('natural_numbers', natural_numbers)
print('sorted_numbers', sorted_numbers)
natural_numbers [1, 10, 3, 4, 20, 30]
sorted_numbers [1, 3, 4, 10, 20, 30]

As you can see, the original list passed as argument, natural_numbers is unchanged.

Caution

If you try to use the built-in function sorted() or the built-in method sort() with a list that contains mixed data-types you will get a TypeError because it is not possible to compare values of different data-types.

print(sorted(mixed_list))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/7f/7nw_x13n5q965rss_qz6061m0000gq/T/ipykernel_46911/4181370301.py in <module>
----> 1 print(sorted(mixed_list))

TypeError: '<' not supported between instances of 'str' and 'int'

5.1.1.6. Searching lists#

The main purpose of searching is to find out whether a list contains the value we are searching for or not. The value that we are searching for is called a key. So in other words, we want to find out whether a list contains a key or not. In Python there is a built-in method that helps us do this:

list_name.index(key_name)

As we have seen many times so far, since index() is a built-in method, we are calling it on a list object; in other words, we search for the key_name inside the list_name. This method will return the first index where the item is found in the list.

Let us look at an example.

fermented_food = ['milk', 'yoghurt', 'beer', 'cider', 'tempeh', 'sauerkraut', 'kefir']
print(fermented_food.index('beer'))
2

Caution

If the key that you are searching for in the list does not exist then you will get a ValueError.

print(fermented_food.index('wine'))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp\ipykernel_19188\831525127.py in <module>
----> 1 print(fermented_food.index('wine'))

ValueError: 'wine' is not in list

Hint

To escape this kind of error Python provides two operators: in and not in. in checks whether an element is part of a list and returns True if so, otherwise False. not in does the opposite, it checks whether an element is not in a list and if it is not, returns True, otherwise it returns False.

print('wine' in fermented_food)
print('wine' not in fermented_food)
False
True

There are also variants of the index() method that allow us to specify the start and end indices, or only one of them in the list that we want to search for. The syntax looks like this:

list_name.index(value, start_index, end_index)

Note that the end_index is non-inclusive.

print(fermented_food.index('beer', 1))
print(fermented_food.index('cider', 2, 5))
2
3

Caution

index() does not allow any keyword arguments so it is not possible to specify only the end_index without the start_index. Also the search goes only in the forward direction (i.e., ascending order) even if you specify negative indices.

Next on, we will see another collection data type: tuples.