## 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 indexes work similar to string indexes. 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)``````
10

We access the first item of `nums` using `nums` 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)

nums = 10
print(nums)``````
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
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
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 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 behavior of Python, that developers learning the language face is shown below:

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

# make a change in l1
l1 = 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 behavior 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 = 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: