A list in Python represents a collection of ordered items. The items can be of varying types such as numbers, strings and even other lists, literally anything that has value can be stored in a list.

Lists are mutable, which means that we can modify them in-place after they are created. In contrast, immutable types like strings and tuples require the creation of a new object in memory each time they are modified. This makes mutable objects more flexible and memory efficient, allowing for faster and more complex data manipulation.

Lists objects are created by enclosing comma separated  items in square brackets []. The following examples demonstrates this.

An empty list:

[]

A list of integers:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

A list of strings:

["America", "China", "Australia", "Brazil", "South Africa"]

A list of lists:

[ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]

A list of mixed types:

[ 1, "Python", [1, 2, 3], ('Python', 'Javascript', 'Html', 'CSS') ]

As shown  above, one way to create an empty list is to simply use empty square brackets, another way to achieve this is by using the builtin list() function. 

list()
//[]

This function can also be used to cast other relevant types into lists,  as shown below:

list('Hello, World!')#string to list
//['H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!']
list((1, 2, 3, 4, 5, 6 ))#tuple to list
//[1, 2, 3, 4, 5, 6]
list({1, 2, 3, 4, 5, 6})#set to list
//[1, 2, 3, 4, 5, 6]

Operations on Lists.

A wide variety of operations can be performed on lists, thanks to their mutable nature, which allows for efficient in-place modifications. This means that the majority of these operations directly modify the original list rather than creating a new one. We will explore some of the most common list operations.

Concatenation and Repetition

The " + " operator is used in list concatenation.  In this operation, all the elements of the second list operand are appended to the end of the first list.

Examples:

[1, 2, 3] + [4, 5, 6]
//[1, 2, 3, 4, 5, 6]
['Python', 'Ruby', 'Javascript', 'Swift'] + ['C', 'C++', 'Java', 'Perl'] 
//['Python', 'Ruby', 'Javascript', 'Swift', 'C', 'C++', 'Java', 'Perl']
[ (1, 2), (3, 4) ] + [ (5, 6), (7, 8) ] + [ (9, 10), (11, 12) ]
//[(1, 2), (3, 4), (5, 6), (7, 8), (9, 10), (11, 12)]

In list repetition, the " * " operator is used with a list operand and an integer operand. The resulting list contains  elements which are  the original list's elements repeated the number of times specified by the integer operand. 

Examples:

[0] * 3
//[0, 0, 0]
[1, 2] * 5
//[1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
['Javascript', 'CSS', 'Html'] * 2
//['Javascript', 'CSS', 'Html', 'Javascript', 'CSS', 'Html']

If 0 is used as the integer operand,  the repetition will result in an empty list.

[1, 2, 3] * 0
//[]

 List repetition is especially used to initialize lists with default values. For example:

[0] *10
//[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[False] *10
//[False, False, False, False, False, False, False, False, False, False]

Indexing, Slicing and Assignment

Each Element in a list has an index through  which it can be accessed. The first element in the list has an index 0 , the second has an index , and so forth. Lists like other sequences  also support negative indexing, in this case, the last element in the list  has an index -1 , the second last has -2, the third from last has -3 and so forth. 

The length of a list is equal to number of items that it contains. We can use the builtin len() function to get the  list's length, as shown below:

len([])
//0
len(["Python"])
//1
len([0, 2, 3, 4])
//5
len([(0,1), (2, 3), (4, 5), (6, 7), (8, 9)])
//5

If a list has a length L, the valid indices including negative and positive indices  starts from  -L and ends at L - 1 . Using an index out of this range will result in an IndexError.

Square braces [] are used to access the element at a given index. For example if we have a list L , the syntax is as follows:

L[index]

Examples:

Accessing the first element:

my_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
my_list[0] # using positive indexing
//0
my_list[-len(my_list)] #Using negative indexing
//0

Accessing the last Element:

my_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
my_list[-1] #Using negative indexing
//9
my_list[len(my_list) - 1 ] #Using positive indexing
//9

accessing  elements using positive indexing:

my_list = ['Python', 'Ruby', 'Javascript', 'Swift', 'C', 'C++', 'Java', 'Perl']
my_list[0]
//'Python'
my_list[1]
//'Ruby'
my_list[2]
//'Javascript'
my_list[3]
//'Swift'
my_list[6]
//'Java'

Accessing elements using negative indexing: 

my_list = ['Python', 'Ruby', 'Javascript', 'Swift', 'C', 'C++', 'Java', 'Perl']
my_list[-1]
//'Perl'
my_list[-2]
//'Java'
my_list[-3]
//'C++'
my_list[-6]
//'Javascript'
my_list[-8]
//'Python'

As we said earlier, using an index outside the valid indices ( -L to L-1 ) will raise an  IndexError:

my_list = ['Python', 'Ruby', 'Javascript', 'Swift', 'C', 'C++', 'Java', 'Perl']
my_list[10]
//IndexError: list index out of range
my_list[-10]
//IndexError: list index out of range

Slicing

While indexing is used to access the element  at a specific index, slicing is used to access all the elements in a given range of indices, a list containing these elements is returned.

The square brackets are still used in slicing, but with a slightly different syntax in order to capture a range. The syntax is as follows;

L[start : stop : step]

All the three arguments, start, stop, and step are integers. The start indicates the index where the slicing will begin, the stop indicates the index where the slicing will end without itself included, and the step indicates the jump value. If any of the arguments are omitted, they take on default values: start defaults to 0, stop defaults to the length of the list , and step defaults to 1.

Examples:

L = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
L[0 : 5 : 1]
//[0, 1, 2, 3, 4]
L[0 : 5 ]#step defaults to 1
//[0, 1, 2, 3, 4]
L[3 : 8]
//[3, 4, 5, 6, 7]
L[0 : 10 : 2]
//[0, 2, 4, 6, 8]
L[ : 7]#start defaults to 0
//[0, 1, 2, 3, 4, 5, 6]
L[5 : ] #stop defaults to the list's length
//[5, 6, 7, 8, 9]
L[ : ] #returns the original list
//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Assignment

Since lists are mutable we can change the value at a given index or slice using assignment. The syntax is as follows:

L[index] = <new value>
L[slice] = <new value>

Doing this overwrites the value at the given index or slice with the new value. If we use slice for assignment, the new value needs to be an iterable such as another list, tuple, set e.t.c.

Index assignment Examples:

L =['C', 'C++', 'Java', 'Perl', 'Kotlin', 'Javascript']
L[0] = 'Python'
print(L)
//['Python', 'C++', 'Java', 'Perl', 'Kotlin', 'Javascript']
L[-1] = 'C#'
print(L)
//['Python', 'C++', 'Java', 'Perl', 'Kotlin', 'C#']
L[2] = 'Html'
print(L)
//['Python', 'C++', 'Html', 'Perl', 'Kotlin', 'C#']

Slice assignment Examples:

L = ['C', 'C++', 'Java', 'Perl', 'Kotlin', 'Javascript']
L[ : 3] = ('PHP', 'Html', 'CSS')
print(L)
//['PHP', 'Html', 'CSS', 'Perl', 'Kotlin', 'Javascript']
L[-3: ] = ['Javascript', 'Jquery', 'Bootstrap']
//['Python', 'Html', 'CSS', 'Javascript', 'Jquery', 'Bootstrap']

Trying to assign on an invalid index or slice will raise an  IndexError.

L =['C', 'C++', 'Java', 'Perl', 'Kotlin', 'Javascript']
L[13] = "blah"
//IndexError: list assignment index out of range

List Methods

The list class offers a variety of practical methods that can be invoked on any list instance. Let us look at some of these methods.

append():

Syntax:

L.append(item)

This method is used to add an item to the end of the list.

Examples:

L = [1, 2, 3, 4]
L.append(5)
print(l)
//[1, 2, 3, 4, 5]
L2 = ['C', 'C++', 'Java', 'C#']
L2.append('Python')
print(L2)
//['C', 'C++', 'Java', 'C#', 'Python']

clear():

Syntax:

L.clear()

Removes all elements from the list, effectively making it an empty list.

Examples:

L = [1, 2, 3, 4, 5]
L.clear()
print(L)
//[]

copy():

Syntax:

L.copy()

The copy() method in creates a shallow copy of the list. This means that a new list object is created with the same elements as the original list, but any mutable elements in the original list (such as lists or dictionaries) are still referenced by both the original and the copied list. 

Examples:

L = [ [1, 2], [3, 4], [5, 6]]
L2 = L.copy()
print(L2)
//[[1, 2], [3, 4], [5, 6]]
L[0][1] = [7]
print(L2)
//[[1, [7]], [3, 4], [5, 6]]

extend():

Syntax:

L.extend(iter)

Adds all elements of an iterable iter (e.g. another list) to the list L.

Examples:

L = [1, 2, 3, 4, 5]
L.extend((6, 7, 8, 9))
print(L)
//[1, 2, 3, 4, 5, 6, 7, 8, 9]

index():

Syntax:

L.index(x)

Returns the position (index) of the leftmost occurrence of value x in the list L, raises a ValueError if the item is not in the list.

Examples:

L = ['Javascript', 'C#', 'C++', 'Python', 'Java']
L.index('Python')
//3
L.index("Ruby")
//ValueError: 'Ruby' is not in list

insert():

Syntax:

L.insert(i, x)

Inserts element x at position (index) i in the list L.

Examples:

L = [1, 2, 3, 5, 6, 7]
L.insert(3, 4)
print(L)
//[1, 2, 3, 4, 5, 6, 7]
L2 = ['Python', 'C#', 'Java', 'PHP']
L2.insert(1, 'C++')
print(L2)
//['Python', 'C++', 'C#', 'Java', 'PHP']

pop()

Syntax:

L.pop(i)

Removes and returns the element at index i, if i is not given it defaults to the last index of the list L .

Examples:

L = [1, 2, 3, 4, 5, 6, 7]
L.pop()
//7
print(L)
[1, 2, 3, 4, 5, 6]
L.pop(2)
//3
print(L)
//[1, 2, 4, 5, 6]

remove():

Syntax:

L.remove(x)

Removes the leftmost occurrence of element x in the list L, raises ValueError if x is not in the list.

Examples:

L = ['Python', 'Ruby', 'C#', 'Javascript', 'C++','PHP']
L.remove('PHP')
print(L)
//['Python', 'Ruby', 'C#', 'Javascript', 'C++']
L.remove('Python')
print (L)
//['Ruby', 'C#', 'Javascript', 'C++']
L.remove('CSS')
//ValueError: list.remove(x): x not in list

reverse():

Syntax:

L.reverse()

Reverses the order of elements in the list L.

Examples:

L = [1, 2, 3, 4, 5, 6, 7]
L.reverse()
print(L)
//[7, 6, 5, 4, 3, 2, 1]

sort():

Syntax:

L.sort()

Sorts the elements in the list L in ascending order.

Examples:

L = [1, 6, 2, 0, 5,3, 4]
L.sort()
print(L)
//[0, 1, 2, 3, 4, 5, 6]