# list: Creating, Accessing, Adding, Removing, Searching and Looping<no value>

## What will we learn?

**list** is the most common data structure in Python. We already used it briefly. Today we will learn it properly:

- Creating it and accessing items.
- Adding, removing and changing.
- Searching.
- Looping and sorting.

## Why do we need this?

Almost every program holds **collections**: a list of students, products in a cart, today's orders, lines from a file, results of a Database query (the backend and databases topics). When the number is not known in advance, you cannot keep a separate variable for each item (`student1`, `student2`...). A list holds **as many as needed**, in a certain order.

## Explanation from the basics

### What is a list?

A **list** is an **ordered** collection of values:

- **Ordered** — every item has a fixed **position**, the **index**.
- **Mutable** — you can add, remove and change items after it is created.
- **Allows duplicates** — the same value can appear several times.
- **Any type** — you can mix them (`[1, "a", True]`), but usually a list holds items of the same kind.

{{< box "analogy" "Analogy: a train" >}}
A list is a train with numbered cars. Every car (item) carries a load (a value). The first car is **number 0**, not 1. You can attach a car at the end (`append`), put a car in the middle (`insert`), or detach a car (`remove`/`pop`).
{{< /box >}}

### Indexes

```text
fruits = ["apple", "banana", "cherry", "date"]
index:       0         1         2        3
negative:   -4        -3        -2       -1
```

- Counting starts from **0**.
- A **negative** index counts from the end: `-1` = the last one, `-2` = the one before last.
- An index that does not exist → `IndexError`.

## Syntax

```python
# create
empty = []
numbers = [10, 20, 30]

# access and change
numbers[0]          # 10 - first item
numbers[-1]         # 30 - last item
numbers[1] = 25     # change an item
len(numbers)        # 3 - how many items

# add
numbers.append(40)       # add to the end
numbers.insert(0, 5)     # insert at index 0, pushing the rest right

# remove
numbers.remove(25)       # remove the FIRST item equal to 25
last = numbers.pop()     # remove and return the last item
first = numbers.pop(0)   # remove and return the item at index 0
del numbers[0]           # delete by index (returns nothing)
numbers.clear()          # remove everything

# search
30 in numbers            # True / False
numbers.index(30)        # index of the first 30 (ValueError if missing)
numbers.count(30)        # how many times 30 appears

# sort
numbers.sort()               # sorts THIS list in place
numbers.sort(reverse=True)   # descending
sorted(numbers)              # returns a NEW sorted list, original unchanged
```

A **method** is a function that "belongs" to an object, and is called with a dot: `numbers.append(40)`. We will learn about methods in depth in the OOP topic. For now: "an action the list knows how to do on itself".

## First code example

`week-2/shopping_list.py`:

```python
shopping = ["milk", "bread", "eggs"]
print("Start:", shopping)

shopping.append("cheese")
print("After append:", shopping)

shopping.insert(1, "butter")
print("After insert:", shopping)

shopping.remove("bread")
print("After remove:", shopping)

print("First item:", shopping[0])
print("Last item:", shopping[-1])
print("Items count:", len(shopping))
```

**Output:**

```text
Start: ['milk', 'bread', 'eggs']
After append: ['milk', 'bread', 'eggs', 'cheese']
After insert: ['milk', 'butter', 'bread', 'eggs', 'cheese']
After remove: ['milk', 'butter', 'eggs', 'cheese']
First item: milk
Last item: cheese
Items count: 4
```

## Breaking down the code line by line

| Line | What happened to the list |
|---|---|
| `shopping = [...]` | Created with 3 items: indexes 0, 1, 2. |
| `.append("cheese")` | Added **at the end**. Now 4 items. |
| `.insert(1, "butter")` | Went in **at index 1**. What was there (`bread`) and what came after it moved right. |
| `.remove("bread")` | Removed **by value**: Python looks for the first occurrence of `"bread"` and removes it. |
| `shopping[0]` | The first item. |
| `shopping[-1]` | The last item, without knowing how many there are. |
| `len(shopping)` | The number of items. |

**Notice:** `append`, `insert` and `remove` **change the list itself** and do not return a new list. That is why you do not write `shopping = shopping.append(...)` (more on that in the mistakes section).

## Another example

**Managing a checkout queue**: looping, searching, `pop` and `enumerate`. `week-2/queue.py`:

```python
queue = ["dana", "avi", "noa", "avi", "yossi"]

print("People in queue:", len(queue))
print("Is noa waiting?", "noa" in queue)
print("noa's position:", queue.index("noa") + 1)
print("How many times avi:", queue.count("avi"))

served = queue.pop(0)
print("Now serving:", served)

print("--- Remaining queue ---")
for position, name in enumerate(queue, start=1):
    print(position, name)

queue.sort()
print("Alphabetical:", queue)
```

**Output:**

```text
People in queue: 5
Is noa waiting? True
noa's position: 3
How many times avi: 2
Now serving: dana
--- Remaining queue ---
1 avi
2 noa
3 avi
4 yossi
Alphabetical: ['avi', 'avi', 'noa', 'yossi']
```

**Breakdown:**

- `"noa" in queue` — `in` goes over the list and checks whether the value exists. It returns a `bool`.
- `queue.index("noa") + 1` — `index` returns the index (2). We add 1 because for people, "third place" starts from 1.
- `queue.pop(0)` — **takes out and returns** the first one. That is the difference from `remove`: `pop` gives you the value that was taken out, so we can use it (`served`).
- `enumerate(queue, start=1)` — in every round it gives a **pair**: a running number and the item. `for position, name in ...` unpacks the pair into two variables. Without `start=1`, counting starts from 0.
- `queue.sort()` — sorts the list **itself**, alphabetically for text and in ascending order for numbers.

## Common mistake

**Mistake 1: assigning the result of a method that changes the list in place.**

```python
names = ["noa", "avi"]
names = names.append("dana")
print(names)
```

```text
None
```

`append` changes the list and **returns `None`**. We assigned the `None` into `names`, and lost the list! The fix: `names.append("dana")` on its own. The same goes for `sort()`, `insert()`, `remove()`.

**Mistake 2: Off-by-one on the last index.**

```python
items = ["a", "b", "c"]
print(items[len(items)])
```

```text
IndexError: list index out of range
```

`len` is 3, but the last index is **2**. The last one = `items[-1]` or `items[len(items) - 1]`.

**Mistake 3: `remove` on a value that does not exist.**

```python
items.remove("z")
```

```text
ValueError: list.remove(x): x not in list
```

Check first: `if "z" in items: items.remove("z")`.

**Mistake 4: removing from a list while looping over it.**

```python
numbers = [1, 2, 2, 3]
for n in numbers:
    if n == 2:
        numbers.remove(n)
print(numbers)
```

```text
[1, 2, 3]
```

We expected `[1, 3]`. When you remove an item, all the items move left, and the loop "skips" the next item. The safe solution: **build a new list** with what you want to keep:

```python
numbers = [1, 2, 2, 3]
kept = []
for n in numbers:
    if n != 2:
        kept.append(n)
print(kept)   # [1, 3]
```

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Practice

The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.

### Exercise 1: Indexes

```python
days = ["Sun", "Mon", "Tue", "Wed", "Thu"]
```

Print, using indexes only: the first day, the last day (with a negative index), and the middle day.

**Expected output:**

```text
Sun
Thu
Tue
```

{{< solution >}}
```python
days = ["Sun", "Mon", "Tue", "Wed", "Thu"]

print(days[0])
print(days[-1])
print(days[2])
```

```text
Sun
Thu
Tue
```

Indexes start from 0. A negative index counts from the end: `-1` is the last item.
{{< /solution >}}

### Exercise 2: A shopping list

Start from `["milk", "bread"]`, and do the following in order:

1. Add `"eggs"` to the end.
2. Add `"butter"` in the first place.
3. Remove `"bread"`.
4. Print the list and the number of items.

**Expected output:**

```text
['butter', 'milk', 'eggs']
3
```

{{< solution >}}
```python
shopping = ["milk", "bread"]

shopping.append("eggs")
shopping.insert(0, "butter")
shopping.remove("bread")

print(shopping)
print(len(shopping))
```

```text
['butter', 'milk', 'eggs']
3
```

- `append` — to the end. `insert(0, ...)` — to a specific position.
- `remove` — removes **by value** (the first occurrence).
{{< /solution >}}

### Exercise 3: A queue

```python
queue = ["dana", "avi", "noa", "avi", "yossi"]
```

1. Check whether `"noa"` is in the queue, and print which place she is in (the first place is 1).
2. Print how many times `"avi"` appears.
3. Take out the first person in the queue and print `Now serving: <name>`, and the number of people who remain.

**Expected output:**

```text
True
3
2
Now serving: dana
4
```

{{< solution >}}
```python
queue = ["dana", "avi", "noa", "avi", "yossi"]

print("noa" in queue)
print(queue.index("noa") + 1)
print(queue.count("avi"))

served = queue.pop(0)
print("Now serving:", served)
print(len(queue))
```

```text
True
3
2
Now serving: dana
4
```

`pop(0)` both removes and **returns** the item — that is why you can save it in a variable.
{{< /solution >}}

### Exercise 4: Statistics on grades

```python
grades = [78, 92, 55, 88, 100, 43, 67]
```

Print the highest grade, the lowest, the average (rounded to one digit), and a new list of the grades that passed (55 and above) — without changing `grades`.

**Expected output:**

```text
Highest: 100
Lowest: 43
Average: 74.7
Passed: [78, 92, 55, 88, 100, 67]
```

{{< solution >}}
```python
PASSING_GRADE = 55

grades = [78, 92, 55, 88, 100, 43, 67]

print("Highest:", max(grades))
print("Lowest:", min(grades))
print("Average:", round(sum(grades) / len(grades), 1))

passed = []
for grade in grades:
    if grade >= PASSING_GRADE:
        passed.append(grade)
print("Passed:", passed)
```

```text
Highest: 100
Lowest: 43
Average: 74.7
Passed: [78, 92, 55, 88, 100, 67]
```

The pattern of "a new list + a loop + `append`" is the way to filter a list without touching the original.
{{< /solution >}}

### Exercise 5: Removing during a loop

```python
numbers = [1, 2, 2, 3, 2, 4]
for n in numbers:
    if n == 2:
        numbers.remove(n)
print(numbers)
```

The intention was to remove all the 2s, but one remained. Write a function `remove_all(items, value)` that returns a new list without any occurrence of `value`.

**Expected output:**

```text
[1, 3, 4]
```

{{< solution >}}
When you remove from a list **while** looping over it, the items move left, and the loop skips the item that moved into the removed place. That is why you build a new list instead:

```python
def remove_all(items, value):
    result = []
    for item in items:
        if item != value:
            result.append(item)
    return result


print(remove_all([1, 2, 2, 3, 2, 4], 2))
```

```text
[1, 3, 4]
```
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **list** = an ordered, mutable collection, with duplicates. Written with `[ ]`.
- An index starts at **0**. `-1` = the last one. An index that does not exist → `IndexError`.
- Adding: `append` (to the end), `insert` (to a position). Removing: `remove` (by value), `pop` (by position, and it returns the item), `del`.
- Searching: `in`, `index`, `count`. Looping: `for`, and `enumerate` when you also need a number.
- Methods that change in place (`append`, `sort`...) return **`None`**. `sorted()` returns a **new** list.
- Do not remove from a list while looping over it. Build a new list.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [Python Tutorial — Lists](https://docs.python.org/3/tutorial/introduction.html#lists)
- [Python Tutorial — More on Lists](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists)
- [Built-in Types — Lists](https://docs.python.org/3/builtins/stdtypes.html#lists)

<!-- docs:end -->
