# Lists

Course: [Python](https://www.devtutorials.ai/learn/python) · Section: Collections
Source: https://www.devtutorials.ai/learn/python/collections/lists

> Creating, indexing and slicing lists, updating and removing items, sorting with sort() and sorted(), shallow against deep copying, and every list method.

Think about a shopping list on a piece of paper.

Items are written one under another, in the order you thought of them. You can add to the bottom, cross something out, squeeze a new item between two others, or read the third one without reading the first two.

That is a **list**: many values, in order, and changeable.

Here is a list of marks in action:

```python runnable title="A list of marks"
marks = [78, 85, 60]

print(marks)
print(marks[0])
print(len(marks))

marks.append(92)

print(marks)
```

Lists are the collection Python programs use most. Nearly everything you did with arrays applies here, with fewer rules and more methods.

A list holds many values in order, and you can change it after you make it.

## What You Will Learn

In this lesson, you will learn:

- How to make a list, and what makes lists different from the other collections
- How to reach one item, or a run of them
- How to change, add and remove items
- How to sort, and the difference between `sort()` and `sorted()`
- Why copying a list is harder than it looks
- Every list method, and the built-ins that work on lists

By the end of this lesson, you will be able to create lists, reach items with indexes and slices, add, change and remove items, sort a list with `sort()` and `sorted()`, copy a list without surprises, and pick the right list method or built-in for each job.

## Python Lists

### Introduction to Lists

A **list** is an ordered, changeable collection. Each item has a position, starting at 0.

### Creating Lists

There are several ways to make a list, and all of them are common:

```python runnable title="Four ways to make a list"
marks = [78, 85, 60]
empty = []
also_empty = list()
from_text = list("Asha")

print(marks)
print(empty, also_empty)
print(from_text)
```

`list()` turns anything you can loop over into a list, which is often how one gets built:

```python runnable title="Building a list from something else"
print(list(range(5)))
print(list("Asha"))
print(list((78, 85)))
print([mark * 2 for mark in [78, 85]])
```

The last line is a **list comprehension** — a loop written inside the brackets. It is the usual way to build a list from another one.

### List Characteristics

<div className="tscroll">

| Characteristic | What it means |
|---|---|
| Ordered | items keep the order you put them in |
| Indexed | the first item is at 0 |
| Changeable | you can replace, add and remove items |
| Duplicates allowed | the same value can appear many times |
| Any type | items need not match |
| Nestable | a list can hold other lists |

</div>

```python runnable title="Duplicates and nesting are both fine"
marks = [78, 78, 85]
nested = [[78, 85], [60, 92]]

print(marks, marks.count(78))
print(nested)
print(nested[0], nested[0][1])
```

### Heterogeneous Lists

A list can hold different types at once. This is where it differs from the `array` module.

```python runnable title="Mixed types in one list"
record = ["Asha", 13, 78.5, True, None, [78, 85]]

for item in record:
    print(type(item).__name__, "->", item)
```

Allowed, and usually a bad idea. A list whose items vary means every loop over it has to check what it has. A dictionary or a class says what each piece is.

### Mutable Nature of Lists

**Mutable** means changeable in place. A list is; a string and a tuple are not.

```python runnable title="A list changes in place; a string cannot"
marks = [78, 85]
marks[0] = 90
print(marks)

name = "Asha"
try:
    name[0] = "M"
except TypeError as error:
    print("A string refuses:", error)
```

That one difference drives most of what follows, especially copying.

## Accessing List Elements

Now that you know how to make a list, the next question is how to reach what is inside one.

### List Indexing

#### Positive Indexing

Count from the front, starting at 0:

```python runnable title="Counting from the front"
marks = [78, 85, 60, 92]

print(marks[0])
print(marks[2])
print(marks[len(marks) - 1])
```

```python runnable expect-error title="One past the end"
marks = [78, 85]
print(marks[2])
```

#### Negative Indexing

You can also count from the back, using a minus sign:

```python runnable title="Counting from the back"
marks = [78, 85, 60, 92]

print(marks[-1])
print(marks[-2])
```

`marks[-1]` is the clearest way to say "the last one".

### List Slicing

```text
marks[start:stop:step]
```

A slice gives back a **new list**, from `start` up to but not including `stop`.

```python runnable title="Taking a run of items"
marks = [78, 85, 60, 92, 45]

print(marks[1:3])
print(marks[:3])
print(marks[2:])
print(marks[-2:])
print(marks[:])
```

### Slice with Steps

```python runnable title="Every second item, and backwards"
marks = [78, 85, 60, 92, 45]

print(marks[::2])
print(marks[1::2])
print(marks[::-1])
```

`[::-1]` is the usual shorthand for a reversed copy.

### Accessing Multiple Elements

A slice can also be **assigned to**, which replaces that run:

```python runnable title="Replacing a run of items"
marks = [78, 85, 60, 92]

marks[1:3] = [100, 200]
print(marks)

marks[1:3] = [1]
print(marks)
```

The replacement need not be the same length, so the list can shrink or grow.

A slice never raises, even when the numbers are out of range:

```python runnable title="A slice out of range is simply empty"
marks = [78, 85]

print(marks[5:10])
print(marks[0:100])
```

## Updating Lists

You can reach any item now, so here is how to change one, or a whole run of them.

### Modifying List Elements

Change a single item by assigning to its position:

```python runnable title="Changing one item"
marks = [78, 85, 60]

marks[1] = 90

print(marks)
```

### Updating Multiple Elements

```python runnable title="Changing several at once"
marks = [78, 85, 60, 92]

marks[0:2] = [10, 20]
print(marks)

marks = [mark + 5 for mark in marks]
print(marks)
```

The second form builds a new list. To change the original in place, loop over the positions:

```python runnable title="Changing every item in place"
marks = [78, 85, 60]

for position in range(len(marks)):
    marks[position] += 5

print(marks)
```

### Adding Elements with append()

`append()` adds **one** item to the end.

```python runnable title="append"
marks = [78, 85]

marks.append(60)
marks.append([92, 45])

print(marks)
print(len(marks))
```

The list given to the second `append` went in as a single item. `extend()` is what adds each one.

### Adding Elements with insert()

`insert()` puts an item at a position you choose instead of the end:

```python runnable title="insert"
marks = [78, 60]

marks.insert(1, 85)
marks.insert(0, 50)

print(marks)
```

## Removing List Elements

Adding is only half of changing a list; here is how to take items out.

### Using del

`del` removes by position and gives nothing back. It can also take a slice.

```python runnable title="del"
marks = [78, 85, 60, 92]

del marks[0]
print(marks)

del marks[0:2]
print(marks)
```

### Using remove()

`remove()` deletes the **first** item equal to a value.

```python runnable title="remove"
marks = [78, 85, 60, 85]

marks.remove(85)
print(marks)
```

```python runnable expect-error title="Removing something that is not there"
marks = [78, 85]
marks.remove(99)
```

### Using pop()

`pop()` removes by position and **hands the item back**.

```python runnable title="pop"
marks = [78, 85, 60]

print(marks.pop())
print(marks.pop(0))
print(marks)
```

### Clearing a List

```python runnable title="Three ways to empty a list"
a = [78, 85]
a.clear()
print(a)

b = [78, 85]
del b[:]
print(b)

c = [78, 85]
c = []
print(c)
```

The first two empty the existing list. The third makes a **new** empty list and points the name at it, which matters when something else is holding the old one:

```python runnable title="Why the difference matters"
original = [78, 85]
other_name = original

original.clear()
print("After clear(), the other name sees:", other_name)

original = [78, 85]
other_name = original

original = []
print("After reassigning, the other name sees:", other_name)
```

## List Operations

### Concatenation (+)

```python runnable title="Joining two lists"
term1 = [78, 85]
term2 = [60, 92]

print(term1 + term2)
print(term1)
```

`+` builds a new list and leaves both originals alone.

### Repetition (*)

```python runnable title="Repeating a list"
print([0] * 5)
print([78, 85] * 2)
```

`[0] * 5` is the usual way to make a list of a known size. Be careful with nested lists:

```python runnable title="A repeated inner list is shared"
grid = [[0] * 3] * 2

print(grid)

grid[0][0] = 99

print(grid)
```

Both rows changed, because `* 2` repeated the **same** inner list twice. Build them separately:

```python runnable title="Each row built on its own"
grid = [[0] * 3 for _ in range(2)]

grid[0][0] = 99

print(grid)
```

### Membership Operators

```python runnable title="in and not in"
marks = [78, 85, 60]

print(78 in marks)
print(99 in marks)
print(99 not in marks)
```

### Iterating Through Lists

```python runnable title="Three ways to loop"
marks = [78, 85, 60]

for mark in marks:
    print(mark)

for position, mark in enumerate(marks, start=1):
    print(position, mark)

for subject, mark in zip(["Maths", "Science", "English"], marks):
    print(subject, mark)
```

**Never change a list's length while looping over it.** The positions shift under the loop:

```python runnable title="Removing while looping skips items"
marks = [20, 15, 85, 60]

for mark in marks:
    if mark < 35:
        marks.remove(mark)

print(marks)
```

15 survived. When 20 was removed, everything shifted left, and the loop's next step skipped straight past it.

```python runnable title="Build a new list instead"
marks = [20, 15, 85, 60]

marks = [mark for mark in marks if mark >= 35]

print(marks)
```

Never add to or remove from a list while looping over it. Build a new list with a comprehension, or loop over a copy with `for mark in marks[:]`.

## Sorting Lists

Sorting a list wrong — or copying one wrong — causes bugs that look impossible. Sorting first:

### Sorting with sort()

`sort()` sorts the list **in place** and returns `None`.

```python runnable title="sort changes the list"
marks = [78, 20, 85]

result = marks.sort()

print("The list now:", marks)
print("What sort() returned:", result)
```

`marks = marks.sort()` would leave you holding `None`.

### Sorting with sorted()

`sorted()` returns a **new** sorted list and leaves the original alone.

```python runnable title="sorted leaves the original"
marks = [78, 20, 85]

ordered = sorted(marks)

print("New list:", ordered)
print("Original:", marks)
```

`sorted()` works on anything you can loop over, not just lists:

```python runnable title="sorted works on more than lists"
print(sorted("Asha"))
print(sorted((78, 20)))
print(sorted({"maths": 78, "science": 85}))
```

### Ascending Order

Ascending is the default.

```python runnable title="Ascending"
print(sorted([78, 20, 85]))
print(sorted(["Science", "Maths", "English"]))
```

### Descending Order and the reverse Parameter

```python runnable title="reverse"
marks = [78, 20, 85]

print(sorted(marks, reverse=True))

marks.sort(reverse=True)
print(marks)
```

`reverse=True` sorts the other way. `list.reverse()` is different — it flips the current order without sorting:

```python runnable title="reverse() is not a descending sort"
marks = [78, 20, 85]

marks.reverse()
print("reverse():", marks)

marks = [78, 20, 85]
marks.sort(reverse=True)
print("sort(reverse=True):", marks)
```

### The key Parameter

`key` takes a function, applied to each item to decide what to sort by.

```python runnable title="Sorting by something other than the value"
subjects = ["Science", "maths", "English"]

print(sorted(subjects))
print(sorted(subjects, key=str.lower))
print(sorted(subjects, key=len))
```

The plain sort put capitals first, because capital letters come before small ones. `key=str.lower` compares them fairly.

```python runnable title="Sorting records by one field"
students = [("Asha", 78), ("Ravi", 85), ("Meera", 60)]

print(sorted(students))
print(sorted(students, key=lambda student: student[1]))
print(sorted(students, key=lambda student: student[1], reverse=True))
```

`lambda student: student[1]` is a small nameless function meaning "use the second item". Without a `key`, tuples sort by their first item.

## Copying Lists

One idea left, and it saves hours of debugging: what happens when you assign a list to a second name.

### Why Copying Matters

A list name holds a reference. Assigning it does **not** make a copy.

```python runnable title="Assignment does not copy"
original = [78, 85]
other = original

other.append(60)

print("original:", original)
print("other:   ", other)
print("Same object?", original is other)
```

One list, two names. This surprises everyone once, and causes bugs that look impossible.

### Assignment vs Copy

```python runnable title="Four ways to actually copy"
original = [78, 85]

a = original.copy()
b = original[:]
c = list(original)

import copy
d = copy.copy(original)

for name, made in [("copy()", a), ("[:]", b), ("list()", c), ("copy.copy()", d)]:
    print(f"{name:<12} equal: {made == original}  same object: {made is original}")
```

All four give a new list with the same items.

### Shallow Copy

All four are **shallow**: the new list is new, but the items inside are the same objects.

```python runnable title="A shallow copy shares what is inside"
original = [[78, 85], [60, 92]]
shallow = original.copy()

shallow[0].append(99)

print("original:", original)
print("shallow: ", shallow)
```

The outer list was copied; the inner lists were not. Changing one through either name changes both.

```python runnable title="Where shallow copying is enough"
original = [78, 85]
shallow = original.copy()

shallow.append(60)
shallow[0] = 90

print("original:", original)
print("shallow: ", shallow)
```

With numbers and text, a shallow copy behaves exactly as you would hope — those values cannot be changed in place.

### Deep Copy

`copy.deepcopy()` copies the inside too, as far down as it goes.

```python runnable title="A deep copy copies all the way down"
import copy

original = [[78, 85], [60, 92]]
deep = copy.deepcopy(original)

deep[0].append(99)

print("original:", original)
print("deep:    ", deep)
```

### list.copy(), copy.copy() and copy.deepcopy()

<div className="tscroll">

| Form | Copies | Use when |
|---|---|---|
| `other = original` | nothing — one list, two names | you want them linked |
| `original.copy()` | the list, not the items | items are numbers or text |
| `original[:]` | the same as `.copy()` | shorter, older style |
| `list(original)` | the same as `.copy()` | also converts other collections |
| `copy.copy(x)` | the same, for any type | the type may not be a list |
| `copy.deepcopy(x)` | everything, all the way down | the list holds lists or objects |

</div>

```python runnable title="All three, on a nested list"
import copy

original = [[78, 85], [60]]

linked = original
shallow = original.copy()
deep = copy.deepcopy(original)

original[0].append(99)

print("linked: ", linked)
print("shallow:", shallow)
print("deep:   ", deep)
```

### Common Pitfalls

```python runnable title="A copy taken too late"
marks = [78, 85]

def add_bonus(values):
    values.append(5)
    return values

result = add_bonus(marks)

print("returned:", result)
print("original:", marks)
```

The function changed the caller's list, because it received a reference. Copy at the start if you mean to leave it alone:

```python runnable title="Copying at the door"
marks = [78, 85]

def add_bonus(values):
    values = list(values)
    values.append(5)
    return values

result = add_bonus(marks)

print("returned:", result)
print("original:", marks)
```

Use `deepcopy` only when the list really holds other lists or objects. It is slow on large structures, and for numbers and text it buys nothing a plain `.copy()` does not give you.

## List Methods

You have now met every list method one at a time. Here they are working together, so you can see the full set:

```python runnable title="Every method that changes the list"
marks = [78]

marks.append(85)
print("append:", marks)

marks.extend([60, 92])
print("extend:", marks)

marks.insert(1, 100)
print("insert:", marks)

marks.remove(100)
print("remove:", marks)

print("pop:   ", marks.pop(), marks)

marks.reverse()
print("reverse:", marks)

marks.sort()
print("sort:   ", marks)

marks.clear()
print("clear:  ", marks)
```

```python runnable title="The methods that answer a question"
marks = [78, 85, 60, 85]

print("count:", marks.count(85))
print("index:", marks.index(85))
print("index from position 2:", marks.index(85, 2))
print("copy:", marks.copy())
```

<div className="tscroll">

| Method | What it does | Gives back |
|---|---|---|
| `append(x)` | adds one item at the end | nothing |
| `extend(xs)` | adds each item of a collection | nothing |
| `insert(i, x)` | inserts before position `i` | nothing |
| `remove(x)` | deletes the first item equal to `x` | nothing |
| `pop(i)` | deletes by position | the removed item |
| `clear()` | empties the list | nothing |
| `copy()` | a shallow copy | a new list |
| `count(x)` | how many times `x` appears | a number |
| `index(x)` | position of the first `x` | a number |
| `reverse()` | flips the order in place | nothing |
| `sort()` | sorts in place | nothing |

</div>

Every method in that table that changes the list returns `None`. `marks = marks.sort()` and `marks = marks.append(5)` both throw the list away.

## Built-in Functions with Lists

Lists also work with many of Python's built-in functions:

```python runnable title="The built-ins that work on a list"
marks = [78, 85, 60, 92]

print("len:   ", len(marks))
print("max:   ", max(marks))
print("min:   ", min(marks))
print("sum:   ", sum(marks))
print("sorted:", sorted(marks))
print("list:  ", list("Asha"))
```

`zip()` walks several collections side by side:

```python runnable title="zip"
subjects = ["Maths", "Science", "English"]
marks = [78, 85, 60]

print(list(zip(subjects, marks)))

for subject, mark in zip(subjects, marks):
    print(subject, mark)

print(dict(zip(subjects, marks)))
```

`zip()` stops at the shortest, with no warning:

```python runnable title="zip stops at the shortest"
print(list(zip([1, 2, 3], ["a", "b"])))
```

Three more worth knowing:

```python runnable title="any, all and a comprehension"
marks = [78, 85, 60]

print(all(mark >= 35 for mark in marks))
print(any(mark >= 80 for mark in marks))
print([mark for mark in marks if mark >= 80])
```

Before moving on, here are some of the mistakes beginners make with lists.

## Common mistakes

Each row is one mistake. The middle column is what you will actually see on screen.

<div className="tscroll">

| Mistake | What you see | Why it happens | Fix |
|---|---|---|---|
| `marks = marks.sort()` | `marks` becomes `None` | `sort()` sorts in place and returns nothing | Call it on its own line |
| `other = original` expecting a copy | Both change together | Assignment copies the reference | `original.copy()` |
| A shallow copy of a nested list | Inner lists still shared | Only the outer list is copied | `copy.deepcopy()` |
| Removing while looping | Items quietly skipped | Positions shift under the loop | Build a new list |
| `[[0] * 3] * 2` | All rows change together | The inner list is repeated, not copied | `[[0] * 3 for _ in range(2)]` |
| `append()` where `extend()` was meant | A list added as one item | `append` adds exactly one thing | `extend` to add each item |
| `remove()` for a missing value | `ValueError` | It needs the value to be present | Check with `in` first |
| `index()` for a missing value | `ValueError` | `index()` raises on a miss | Check with `in` first |
| Using the length as the last index | `IndexError` | The last index is one less | `marks[-1]` |
| Changing a list passed into a function | The caller's list changes | It was passed by reference | Copy at the start of the function |
| `sort()` on mixed types | `TypeError: '<' not supported` | Numbers and text cannot be compared | Keep one type, or use `key` |

</div>

## What to Remember

- **A list holds many values in order, and can be changed after it is made.**
- Items may be of any type and may repeat; a list can hold other lists.
- The first index is 0 and the last is one less than the length. `marks[-1]` is the last item.
- A slice gives a new list, stops before its second number, and never raises when out of range.
- `append()` adds one item, `extend()` adds each item of a collection.
- `remove()` works by value, `pop()` by position and hands the item back, `del` by position and gives nothing.
- **`sort()` changes the list and returns `None`; `sorted()` returns a new list.**
- `key=` decides what to sort by; `reverse=True` sorts the other way.
- **Assignment does not copy.** `.copy()`, `[:]` and `list()` all make a shallow copy.
- A shallow copy shares the inner objects — use `copy.deepcopy()` for nested data.
- **Never add to or remove from a list while looping over it.** Build a new one.
- `[[0] * 3] * 2` shares one inner list; use a comprehension instead.

Next: **Tuples**
