# Python Lists

## Exploring Lists in Python

In Python, a list is a versatile collection type used to store multiple values into a single variable. Lists are represented by an opening square bracket `[`, followed by the elements separated by commas, and finally, a closing square bracket `]`. Let's dive into the fascinating world of lists and explore their various capabilities.

### List Basics

A list is not limited to a specific number of values when defined. It can always be modified, making it a mutable data structure. This means that you can add, remove, or modify elements within a list after it has been created. Let's start by creating a simple list:

```python
my_list = [1, 2, 3, 4, 5]
```

In the example above, `my_list` contains five elements: 1, 2, 3, 4, and 5. However, lists are not limited to containing elements of a single data type. You can store values of different data types within the same list. Here's an example:

```python
mixed_list = [1, "apple", True, 3.14, ["nested", "list"]]
```

In `mixed_list`, we have integers, a string, a boolean value, a float, and even another list nested within it. This flexibility allows you to store diverse data within a single list.

### Accessing List Elements with Indexing

Each element in a list is assigned a numeric index that identifies its position within the list. The indices start from 0 and go up to the total number of elements minus one. For example, in `my_list`, the element `1` is at index `0`, `2` is at index `1`, and so on. You can access individual elements by using their respective indices. Here's an example:

```python
first_element = my_list[0]
```

In this case, `first_element` will be assigned the value `1`, as it corresponds to the element at index `0` in `my_list`. Remember that indexing starts from `0`, so the index value always lags behind the element position by `1`.

### Negative indexing

in Python allows you to access elements in a list from the end, rather than from the beginning. It provides a convenient way to retrieve elements based on their position relative to the end of the list.

In a list with `n` elements, the negative indices range from `-1` to `-n`, with `-1` representing the last element and `-n` representing the first element. Here's an example to illustrate negative indexing:

```python
my_list = [1, 2, 3, 4, 5]
```

In this case, `my_list` contains five elements. Using positive indexing, we can access the first element with `my_list[0]` and the last element with `my_list[4]`. Negative indexing allows us to achieve the same results using `-1` and `-5`, respectively:

```python
print(my_list[-1])  # Output: 5
print(my_list[-5])  # Output: 1
```

Negative indexing is particularly useful when you want to access elements from the end of a list without knowing its length in advance. For example, if you have a large list and want to access the last few elements, you can simply use negative indices instead of counting the number of elements.

Let's consider a larger list:

```python
my_list = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
```

Using negative indexing, we can access the last three elements easily:

```python
print(my_list[-1])   # Output: 100
print(my_list[-2])   # Output: 90
print(my_list[-3])   # Output: 80
```

Negative indexing simplifies the process of accessing elements from the end of a list and provides a more intuitive way to work with lists in certain scenarios.

### Slicing Lists

Slicing allows you to extract a sublist from a list by specifying a range of indices. It provides a convenient way to work with a portion of a list. The general syntax for slicing is `list_name[start_index:stop_index]`, where `start_index` is the index of the first element to include, and `stop_index` is the index of the first element to exclude. Let's explore some examples:

```python
my_list = [1, 2, 3, 4, 5]
mixed_list = [1, "apple", True, 3.14, ["nested", "list"]]

my_list.extend(mixed_list)

# Slicing from index 1 to index 7 (exclusive)
slice = my_list[1:7]
print(slice)  # Output: [2, 3, 4, 5, 1, 'apple']

# Slicing from the beginning to index 7 (exclusive)
slice2 = my_list[:7]
print(slice2)  # Output: [1, 2, 3, 4, 5, 1, 'apple']

# Slicing from index 6 to the end
slice3 = my_list[6:]
print(slice3)  # Output: [True, 3.14, ['nested', 'list']]

# Slicing with a step of 2
slice4 = my_list[0:8:2]
print(slice4)  # Output: [1, 3, 5, 'apple']
```

In the examples above, we performed different slices on the `my_list` after extending it with `mixed_list`. The first example demonstrates slicing from index 1 to 7 (exclusive), resulting in \`\[2, 3, 4, 5, 1, 'apple

'\]`. The second example slices from the beginning to index 7 (exclusive), giving` \[1, 2, 3, 4, 5, 1, 'apple'\]`. The third example slices from index 6 to the end, producing` \[True, 3.14, \['nested', 'list'\]\]`. Lastly, the fourth example slices with a step of 2, resulting in` \[1, 3, 5, 'apple'\]\`.

Slicing provides flexibility in extracting specific portions of a list based on the indices specified.

### Adding Elements to a List

Lists provide several methods to add elements to them dynamically. Let's explore some commonly used methods:

#### 1\. Append Method

The `append()` method allows you to add an element at the end of a list. Here's an example:

```python
my_list.append(6)
```

After executing this code, `my_list` will be `[1, 2, 3, 4, 5, 6]`. The `append()` method modifies the list in place.

#### 2\. Insert Method

The `insert()` method enables you to add an element at a specific index within the list. It takes two arguments

: the index at which to insert the element and the value of the element itself. Here's an example:

```python
my_list.insert(3, "new value")
```

After executing this code, `my_list` will become `[1, 2, 3, 'new value', 4, 5]`. The `insert()` method shifts the existing elements to make room for the new element.

#### 3\. Extend Method

The `extend()` method allows you to append multiple elements to a list. It takes an iterable (such as another list) as an argument and adds each element of the iterable to the end of the list. Here's an example:

```python
my_list.extend([7, 8, 9])
```

After executing this code, `my_list` will be `[1, 2, 3, 'new value', 4, 5, 7, 8, 9]`. The `extend()` method modifies the list in place.

### Removing Elements from a List

Just as we can add elements, lists also provide methods to remove elements from them. Let's explore two common methods for removing elements:

#### 1\. Pop Method

The `pop()` method removes and returns the last element of a list. It also accepts an optional index as an argument. If an index is provided, `pop()` removes and returns the element at that index. Here's an example:

```python
popped_element = my_list.pop()
```

In this case, `popped_element` will be assigned the value `9`, and `my_list` will become `[1, 2, 3, 'new value', 4, 5, 7, 8]`. If we had passed an index to `pop()`, it would have removed and returned the element at that index.

#### 2\. Remove Method

The `remove()` method removes the first occurrence of a specified value from a list. It takes the value to be removed as an argument. Here's an example:

```python
my_list.remove('new value')
```

After executing this code, `my_list` will be `[1, 2, 3, 4, 5, 7, 8]`, with the `'new value'` element removed.

### Creating a Copy of a List

Sometimes, you may need to create a separate copy of a list without modifying the original list. To accomplish this, you can use the `copy()` method or the slice operator `[:]`. Both methods create a new list with the same elements as the original. Here are the two approaches:

```python
# Using the copy() method
new_list = my_list.copy()

# Using the slice operator
new_list = my_list[:]
```

Now, `new_list` will be an independent copy of `my_list`.

### Looping Through a List

Iterating over a list is a common operation in Python. You can use a `for` loop to iterate through each element of a list and perform specific actions. Let's see an example:

```python
for element in my_list:
    print(element)
```

This loop will print each element of `my_list` on a separate line. You can replace the `print()` statement with any desired action.

### Conclusion

In this lesson, we explored the fundamentals of lists in Python. We learned how to create lists, access their elements using indexing, extract sublists using slicing, add elements using `append()`, `insert()`, and `extend()`, remove elements using `pop()` and `remove()`, create copies of lists, and loop through lists using `for` loops. Lists are powerful and versatile data structures that can be used

to store and manipulate collections of values efficiently.

Keep practising and experimenting with lists to become more comfortable and proficient in using them. Happy coding!

### Practice Questions

1. How can you extract a sublist from index 3 to index 8 (exclusive) from `my_list`?
    
2. What is the result of `my_list[-3:]`?
    
3. How can you create a new list that contains only the last three elements of `my_list`?
    
4. Explain the concept of negative indexing and how it can be used in slicing.
    
5. How would you extract a sublist from index 2 to the end of `my_list` using slicing?
