Introduction

All over programming languages, working with arrays is more than just a common task. This is because a ton of algorithms on sorting, searching, graphs, trees can all be based on arrays.

Even on an elementary level, it's not surprising to save a list of numbers, a list of strings, a list of food items in an array. The array holds all the data under one single location and provides facilities to access each item.

To boil it all down, arrays play a paramount role in computer programming.

As far as Python is concerned, it also provides arrays at the dispense of developers. However, it doesn't call them 'arrays' — rather it refers to them as lists.

In Python, a list is a sequential data type. Specifically:

A list is a sequence of items each stored at a given position known as the index of the item.

List indices work similar to string indices. The nth item of the list is at index n - 1.

For example, the first item is at index 0, the second is as index 1, the third is at index 2, and so on and so forth.

Creating a list

To create a list in Python, we use a pair of square brackets and put the individual items of the list inside it. Different items are separated by a comma.

nums = [10, 20, 30]

Here we've created a list containing three items — 10, 20 and 30 — and assigned it to the variable nums.

Once a list is created, it's rare to never ever use it.

To access an item in a list, we start by writing the identifier holding the list, followed by a pair of square brackets. Inside these, we put the index of the item we wish to retrieve.

Consider the code below:

nums = [10, 20, 30]

print(nums[0])
10

We access the first item of nums using nums[0] and print it. Note the index 0 inside the square brackets — it's the index of the first item.

To change the value of a list item, just use the same bracket syntax as shown above, followed by the = assignment operator, followed by the new value you wish to change the item to.

Below we change the first item of nums to 10:

nums = [1, 2, 3, 4, 5]

print(nums[0])

nums[0] = 10
print(nums[0])
1
10

To get the total number of items in a list, use the len() function, just like it's used on strings.

nums = [10, 20, 30]
print(len(nums))
3

Removing elements

To remove an item from a list, we have two options. Either use the remove() method or the del keyword. Let's first explore del.

If called on a list item, the del keyword deletes the item from the list, shifting down items if the need be.

To delete an item at index i from a list l, we call del l[i]

In the following code we delete the third item from nums using del:

nums = [10, 20, 30]

del nums[2]
print(nums)
[10, 20]

Since the third item was the last item of nums, deleting it didn't require other items to be shifted down by one index.

This shifting is required in places where there are items following the deleted item, such as in the code below:

nums = [10, 20, 30]

del nums[0]
print(nums)
[20, 30]

Here we are deleting the first item from nums. Since more items follow it, when it's deleted, all the following items are shifted one place back. The item at index 1 comes at index 0, the one at index 2 comes at index 1 and so on.

If this hadn't been done, deleting items using del would have left empty holes in lists, which sounds senseless!

Loosely typed

Lists in Python, unlike arrays in languages such as Java, C#, are loosely typed. That is, there is no restriction at all on the type of values that can be stored inside a list.

A list can have elements of literally any type.

Below we create a list values holding five different types of values: a string, an integer, a float, a Boolean, and a tuple:

values = ["Hello", 10, 3.5, True, (1, 3)]

This diversity that Python provides in lists is both good and bad in one way. The good thing is that it gives developers a lot of freedom over constructing lists.

The bad thing is that due to this loosely typed nature of lists, a lot of overhead is stored for each item, even if all the items are of the same type.

For strictly-typed lists, where each item has the same type, we have the array module which we shall explore later in this unit.

Adding new elements

Adding new elements to a list is possible in Python unlike some languages such as C++, C#, Java, etc. This is referred to as variable-width lists.

There are mainly two ways to add an element to a list in Python — either use the append() method or use the insert() method.

Both of these methods are demonstrated below:

append()

The append() method takes an argument and adds it to the end of a given list.

In the code below, we add the number 40 to the list nums:

nums = [10, 20, 30]

nums.append(40)

print(nums)
[10, 20, 30, 40]

Note that it's not possible to add multiple elements to a list using a single call to append, as is otherwise possible in some other languages:

nums = [10, 20, 30]
nums.append(40, 50, 60)
Traceback (most recent call last): File "<stdin>", line 2, in <module> nums.append(40, 50, 60) TypeError: append() takes exactly one argument (3 given)

Also note that it's necessary to provide an argument to append() — without one, it would throw an error.

Take a look at the code below:

nums = [10, 20, 30]
nums.append()
Traceback (most recent call last): File "<stdin>", line 2, in <module> nums.append() TypeError: append() takes exactly one argument (0 given)

We call append() without an argument in line 2, and likewise get an error thrown.

'Push' means 'append' in Python

A much more common term used to refer to the action of adding an element to the end of a list (or array) is 'push'. Most programming languages use this name in methods or functions that do the same job as append() in Python.

Python just like to take a new dimension by using the name 'append'.

insert()

The second way to add an element to a list is to use the insert() method.

It works similar to append() in that it also takes as argument the value to be added to the list. However, this argument is not the first one. The first argument specifies the index where to add the value, passed as the second argument.

Remember that insert() doesn't perform any replacements. It always adds an item at a given position, shifting all the elements beyond that position one step forwards.

Below we add 40 at the start of nums:

nums = [10, 20, 30]
nums.insert(0, 40)

print(nums)
[40, 10, 20, 30]

The value 40 gets added to nums at index 0. 10, which was previously accessible at this index, is now accessible at the next index i.e 1. The same goes for 20 and 30

Similarly, in the code below we add 40 at index 1 in the same list nums:

nums = [10, 20, 30]
nums.append(1, 40)

print(nums)
[10, 40, 20, 30]

Searching for elements

It's a common task to search for something within a list. In Python, this can be done using the in keyword.

The general syntax of in is shown below:

value in iterable

The left operand states the value to search for in the given iterable, which is given as the right operand. A Boolean value is returned indicating whether or not the value exists in the iterable.

In this case, iterable would be a list.

A couple of examples using the in keyword are shown as follows:

nums = [10, 20, 30]

print(30 in nums)
print('30' in nums)
True
False

30 exists in nums, therefore 30 in nums evaluates to True. However, '30' doesn't exist in the list and consequently, '30' in nums returns False.

Following we check for 'London' and 'LONDON' in a list of cities:

cities = ['London', 'Paris', 'Berlin']

print('London' in cities)
print('LONDON' in cities)
True
False

Keep in mind, that the comparison made by in is using the == operator. That is, for each element item, the expression value == item is evaluated. If it returns True, this implies that a match is found, ultimately resolving the in operation by True.

If all elements of the list are exhausted and no matches are found, the in operation evaluates to False.

This type of searching is known as linear search, or sequential search.

To make sure that a certain value doesn't exist in a list, we can use the same in operator along with the not keyword.

not in returns True if the given value is not in the list, or else False. It's simply a negation of the in operation.

nums = [10, 20, 30]

print(30 not in nums)
print('30' not in nums)
False
True

Checking for an empty list

In Python, an empty list is considered a falsey value. This means that when coerced into a Boolean, an empty list would convert to False.

bool([])
False

Similarly, a non-empty list coerces to True.

bool([1, 2])
True

This idea is useful when we want to add elements to a list only if it is empty. Consider the code below.

nums = []

if not nums:
    # add a 0 to nums, if it's empty
    nums.append(0)

First we check whether the list nums is empty. If it is, we append a 0 to it. The expression not nums returns True when nums coerces to False — which happens when it's empty.

Although the example shown above is not really practical, it does demonstrate how to check for an empty list, using just the list itself.

If you think on it, there is one more way to do this. That is using the len() function. To check if a list is empty, we simply check if its length is equal to 0.

If it is, we know that the list has no elements.

nums = []

if len(nums) == 0:
    # add a 0 to nums, if it's empty
    nums.append(0)

Use whichever way suits you; sometimes the coercion method works well if we read the code out loud. Similarly, sometimes it's the length-check method that seems the best choice.

References

One strange behaviour of Python, that developers learning the language face is shown below:

l1 = [1, 3, 5]
l2 = l1

# make a change in l1
l1[0] = 10

print(l2)
[10, 3, 5]

First a list is assigned to the variable l1. Then l1 is assigned to another variable l2. Now a modification is made in l.

Ideally, we've only modified l1 and so want the change to happen in it, only. However, what happens is that both l1 and l2 are found changed; not just l1.

What's this? Is this some kind of an error?

Well, first of all this is perfectly normal behaviour arising from the fact that lists in Python are stored by references.

Let's see what's the big idea?

Strings, numbers, and Booleans are stored by values. That is, inside a variable of one of these types, the actual data resides. In contrast to this, lists and almost all other data types, are stored by references.

That is, a variable holding a list doesn't hold the actual data of the list. Rather, it holds a reference to a location in memory where the list's data resides.

We'll see why is this approach taken for lists, but first let's resolve the confusion in the code above.

l1 = [1, 3, 5]
l2 = l1

# make a change in l1
l1[0] = 10

print(l2)

We create a list [1, 3, 5] and put it in the variable l1. Python creates this list in memory, and then saves its location inside the variable l1. Now l1 holds the reference to the list [1, 3, 5]NOT the actual list.

Next up, we assign l1 to a new variable l2. Since l1 holds a reference, l2 will get to hold this very reference. At this stage, both l1 and l2 hold the same reference.

Now, we change something in the list l1. These changes are made exactly where l1 points to. That is, the changes are made in the list [1, 3, 5] stored in memory.

Next up, accessing l2 returns this modified list, since l2 holds a reference to this same list.

The other way round, if we change something in the list using the variable l2, the changes would be visible in l1, as well. Why?

Simply because both variables l1 and l2 hold references to the exact same location in memory, where the list [1, 3, 5] resides.

Now the question is what's the point of giving this feature.

Why use references?

Memory efficiency.

If this reference mechanism wasn't built into Python, assigning a list variable to another variable would have ended up creating a copy of the list. Passing a list to a function as an argument would've also ended up creating a new list even if the job of the function was to only run through the list.

To boil it down, there would've been so many useless copies of lists in memory, that the program's performance would've been compromised to a considerable extent on machines with already less amounts of memory.

The solution: create a list once, and pass on its references.

One interesting question that comes up at this point is that what if a copy is desired. What if someone does want to make a new list similar to one in memory?

Fortunately, there are ways.

For simple 1D lists, copies can be made by slicing the lists. For deeper 2D or 3D lists, copies can be made by means of the copy module.

To make a copy of a simple 1D list, as shown below, slice the list from its start to its end:

l1 = [1, 3, 5]
l2 = l1[:] # make a copy of l1

When a list is sliced, what happens is that every element of the list is put inside a new list.

Simple cloning cases can be solved just by using list slices. Other complex cases need complex methods.

An example is shown below:

matrix = [[1, 5], [7, 1], [0, 0]]

Here we have a 3 x 2 matrix, represented using a list of lists. To truly copy this list, slicing is not one of the candidates.

Here's why?

matrix[:] surely returns a copy of m. However, every element of this new list is the same element of matrix, all of which are lists themselves. That is, if we make a change in one of the elements of m, that would be seen in the same element in the copied list.

This is because matrix[:] merely assigns each element of matrix to the corresponding element in the new list. And since these elements are lists themselves, they get passed as references.

To copy deeper lists as well, we ought to use the deepcopy() function of the copy module.

This can be seen below:

import copy

matrix = [[1, 5], [7, 1], [0, 0]]

matrix2 = copy.deepcopy(matrix)

The function copies every list is encounters while running over a main list. The result is a truly copied list, having no sort of attachment with the original list.

Keep in mind that deepcopy() is an expensive operation. So use it only when you really want a true copy of a complex list!

The list() function

Apart from denoting lists literally, we can create them using the list() function.

It works by taking an iterable sequence and putting each of its elements in a list, finally returning the list.

list([iterable])

For instance, if we call list() on a string, we will get a list of all its characters, since a string is a sequence of characters and list() takes each item of a sequence and makes it an element of the newly-created list.

Consider the code below:

s = 'Hello'
print(list(s))
['H', 'e', 'l', 'l', 'o']

Calling list() on strings is a common routine in Python programs when each character of a string needs to be modified and processed separately.

Below we split a string of space-delimited numbers into a list, and then work with each element of the list separately:

nums = '10 11 12'
nums_list = nums.split()

for num in nums_list:
    if int(num) % 3 == 0:
        print('Yes')
    else:
        print('No')

nums_list is a list of stringified numbers — ['10', '20', '30']. In the for loop, we retrieve each of these strings, convert it to an integer, and then check if it is divisible by 3.

If it is divisible, 'Yes' is printed, or else 'No' is printed.

No
No
Yes

Immutable in nature

Recall from the Python String Basics chapter, that strings in Python are immutable in nature. That is, a string, once created, can't be changed.

Lists are the opposite of this — they are immutable in nature.

Once a list is created, it can be altered. This can happen by adding new elements, changing existing elements, and by deleting elements.

We've seen how to perform all three of these operations in the discussion above. When we perform either of these operations on a list, we are in effect mutating the list.

Mutating simply means that we are changing the list. Hence, we say that lists are mutable.

The word 'mutable' comes from the word 'mutate', not from the word 'mute'.

Let's review all of them once again: