Skip to content

Lists

A variable holds one thing. A list holds many things in order, all under one name. A leaderboard is a ranked sequence of scores. A quiz is a collection of questions. Once you need to manage a group of related values, you need a list.

Lists are Python's general-purpose ordered, mutable sequence. They are the natural fit for anything that changes over time: items added or removed, order shuffled, contents filtered or sorted. When order matters and the collection changes, a list is usually the right first choice.

list is Python's dynamic array: an ordered, mutable sequence backed by a contiguous heap allocation. Random access is O(1). append() is amortised O(1) because the array over-allocates and grows on overflow. insert() and remove() are O(n) because they shift subsequent elements. These costs should guide decisions about when to prefer other structures.

Creating a list

Square brackets, values separated by commas. Lists can hold any mix of types, and an empty list is valid and common as a starting point you build up over time.

Lists are defined with bracket syntax and preserve insertion order. They can hold any Python value, including other lists. The empty list [] is the standard starting point when you accumulate items incrementally.

The bracket literal allocates a new list object on the heap with some pre-allocated capacity. Elements can be any Python object; the list stores references, not values directly. Heterogeneous element types are valid but uncommon in practice outside of quick scripts.

python
scores   = [87, 92, 74, 65, 91]
players  = ["Alice", "Bob", "Charlie"]
mixed    = ["Alice", 87, True, 3.14]   # any types, though uncommon
empty    = []

Indexing and slicing

Lists use the same numbering as strings: positions start at 0, negative numbers count from the end. You read any item by its position. Because lists are mutable, you can also write to a specific position.

List indexing and slicing follow the same rules as strings. The key difference is mutability: you can assign to an index or a slice to change items in place, something strings do not allow.

list.__getitem__ accepts integers and slice objects, following the same clamping rules as str. __setitem__ enables item assignment. Slice assignment replaces a range of elements and can resize the list if the replacement has a different length: lst[1:3] = [10, 20, 30] replaces two items with three.

python
scores = [87, 92, 74, 65, 91]

scores[0]      # 87  (first)
scores[-1]     # 91  (last)
scores[1:3]    # [92, 74]
scores[:2]     # [87, 92]
scores[::-1]   # [91, 65, 74, 92, 87]  (reversed)

scores[0] = 90   # mutable: works (strings would raise TypeError)

Adding items

Three methods to add items. append() adds a single item to the end and is what you will use almost every time. insert() adds at a specific position. extend() merges another list in.

append() is O(1) amortised and is the standard way to build a list item by item. insert() is O(n) because it shifts subsequent elements. extend() is equivalent to += and more efficient than repeated append() inside a loop.

append() uses the pre-allocated buffer and only copies when overflow triggers a resize. The growth factor is roughly 1.125x after the first few resizes, giving amortised O(1). insert(0, x) is O(n): every element shifts right. For frequent front insertions, collections.deque provides O(1) appendleft. extend(iterable) calls __iter__ once and grows in one operation.

python
scores = [87, 92, 74]

scores.append(65)          # [87, 92, 74, 65]
scores.insert(1, 100)      # [87, 100, 92, 74, 65]
scores.extend([55, 71])    # [87, 100, 92, 74, 65, 55, 71]

A common mistake: append() with a list adds the whole list as one item, giving you a list inside a list. Use extend() to merge instead:

append(x) always adds x as a single element. Passing a list to append() gives you a nested list. Use extend() when you want to merge all items from another list into this one:

append(x) calls list_append with x as a single object regardless of type. extend(iterable) calls __iter__ on the argument and adds each element individually. The += operator calls __iadd__, which calls extend under the hood.

python
scores.append([55, 71])    # [..., [55, 71]]  nested list, probably wrong
scores.extend([55, 71])    # [..., 55, 71]    merged, correct

Removing items

Four tools to remove items. remove() searches by value. pop() removes by position and hands you the item back. del removes by position without a return value. clear() empties the whole list.

remove() is O(n): it scans for the first occurrence by value. pop() without an argument is O(1) for the last item. pop(i) for any other position is O(n) because elements shift. del scores[i] is equivalent to pop(i) but discards the return value.

remove(value) calls __eq__ on each element until it finds a match, then shifts all subsequent elements left: O(n). pop(-1) is O(1), no shifting needed. pop(i) for any other index is O(n). For frequent removals from arbitrary positions, consider restructuring your data or using a different collection.

python
scores = [87, 92, 74, 65, 91]

scores.remove(74)    # removes first occurrence of 74
scores.pop()         # removes and returns last item (91)
scores.pop(0)        # removes and returns item at position 0 (87)
del scores[1]        # removes at position 1, no return value
scores.clear()       # removes everything

remove() raises a ValueError if the value is not in the list. Check with in first if you are not certain:

python
if 74 in scores:
    scores.remove(74)

remove() raises ValueError on a miss. The in check adds an extra O(n) scan; you do two passes. For single-use code this is fine. Proper error handling with try/except ValueError is covered in the Files and exceptions chapter.

The in + remove() pattern is two O(n) scans. When order does not need to be preserved, a faster approach is to swap the target with the last element and pop: O(1). For set membership with O(1) lookup, use set instead of list, covered in the Tuples, sets chapter.

Sorting

sorted() returns a brand new sorted list and leaves your original untouched. .sort() sorts the list in place and returns None. That difference matters more than it sounds.

sorted() is the safe default: it never modifies the original. .sort() modifies in place and returns None, which is a common trap. Assigning the result of .sort() gives you None, not the sorted list. Use sorted() when you need the original intact; use .sort() when you only want the sorted version.

Both use Timsort: a hybrid merge/insertion sort, O(n log n) worst case and O(n) on nearly sorted data. Timsort is stable: equal elements preserve their original relative order. .sort() returns None deliberately (command-query separation). sorted() accepts any iterable, not just lists, and always returns a list.

python
scores = [87, 42, 96, 55, 71]

ranked = sorted(scores)            # [42, 55, 71, 87, 96] (new list)
scores.sort()                      # sorts in place, returns None
scores.sort(reverse=True)          # [96, 87, 71, 55, 42]

result = scores.sort()             # result is None, not the sorted list

Useful operations

Python has a set of built-in tools that work directly on lists. len(), sum(), min(), and max() are the four you will reach for constantly.

The built-in sequence functions work on any list. in is a linear scan for lists; if you need fast repeated membership tests, convert to a set. .index() raises ValueError if the value is not found.

len(), sum(), min(), max() all call __iter__ and work on any iterable, not just lists. in is O(n) for lists; for O(1) lookup, use set. .index(value) is also O(n). sum() defaults to start=0 and does not support string concatenation; use "".join() for strings.

python
scores = [87, 92, 74, 65, 91]

len(scores)          # 5
sum(scores)          # 409
min(scores)          # 65
max(scores)          # 92
scores.count(87)     # 1
scores.index(74)     # 2
74 in scores         # True
74 not in scores     # False
scores.copy()        # shallow copy
scores.reverse()     # reverses in place

Iterating

A for loop goes through a list one item at a time. The variable after for receives each item in turn. When you also need the position, enumerate() gives you both without a manual counter.

for item in list invokes the list's iterator and advances it on each step. enumerate(iterable, start=0) wraps the iterator and yields (index, value) pairs. Using enumerate() is cleaner and less error-prone than maintaining a counter variable.

for invokes iter(list) to get a list_iterator, then calls next() on it until StopIteration. enumerate() wraps any iterator and yields (i, value) pairs. The start parameter offsets the counter but does not affect the underlying index. Unpacking i, item = pair works because enumerate yields tuples.

python
players = ["Alice", "Bob", "Charlie"]

for player in players:
    print(player)

for i, player in enumerate(players, start=1):
    print(f"{i}. {player}")
# 1. Alice
# 2. Bob
# 3. Charlie

for loops and enumerate

for and enumerate() are covered fully in the Control flow chapter. Short version: for player in players runs once per item; enumerate() gives you both the position and the value on every iteration.

Nested lists

A list can contain other lists. This is how you represent a grid or a table: a list of rows, each row being a list of values. Two sets of square brackets access an item: the first picks the row, the second picks the column.

Nested lists are lists of list references. Each inner list is an independent object. Access with chained subscripts: grid[row][col]. Mutating an inner list affects the outer list because the outer list holds a reference to the same object.

Nested lists are not a true 2D array: the outer list holds object references, and inner lists can be different lengths and types. Access chains two __getitem__ calls. Shallow copy of a nested list copies the outer container but not the inner lists; modifications to inner lists affect both copies.

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

grid[0]       # [1, 2, 3]
grid[1][2]    # 6  (row 1, column 2)

Mutability: the gotcha

This surprises almost everyone. Assigning a list to a new variable does not make a copy. Both names point at the same list. Change one and you change the other. To get an independent copy, you have to ask for one explicitly.

List assignment copies the reference, not the object. Both names point at the same underlying list. Mutations through either name affect the same data. When you need independent data, copy explicitly with .copy(), list(), or a full slice [:].

b = a binds a second name to the same list object. Any mutation through b mutates the object a is also pointing at. .copy() and a[:] create a shallow copy: a new list object with the same element references. For flat lists of immutable values this is safe; for nested lists, the inner objects are still shared.

python
a = [1, 2, 3]
b = a            # b is not a copy; it points at the same list

b.append(4)
print(a)         # [1, 2, 3, 4]  (changed: a and b are the same list)
python
b = a.copy()    # independent copy
b = list(a)     # same result
b = a[:]        # also the same

# Nested lists still share their inner objects:
matrix = [[1, 2], [3, 4]]
copy   = matrix.copy()

copy[0].append(99)
print(matrix)   # [[1, 2, 99], [3, 4]]  (inner list was shared)

For nested structures where you need full independence, copy each inner list manually, or use copy.deepcopy() from the standard library, covered in the Modules chapter.

More methods

MethodWhat it does
.append(item)Add to the end
.insert(i, item)Insert at position i
.extend(iterable)Add all items from an iterable
.remove(value)Remove first occurrence of value
.pop(i)Remove and return item at position i (default: last)
.clear()Remove all items
.index(value)Position of first occurrence
.count(value)Number of occurrences
.sort()Sort in place
.reverse()Reverse in place
.copy()Return a shallow copy

In practice

Building a score tracker: add results, sort them, and print a summary.

python
scores = []

scores.append(87)
scores.append(54)
scores.append(92)
scores.append(67)
scores.append(45)

scores.sort(reverse=True)

print(f"Ranked scores: {scores}")
print(f"Highest: {scores[0]}")
print(f"Lowest:  {scores[-1]}")
print(f"Average: {sum(scores) / len(scores):.1f}")
print(f"Top 3:   {scores[:3]}")

Two parallel lists of names and scores: find the top performer and print ranked results.

python
names  = ["Alice", "Bob", "Carol", "Dave"]
scores = [87, 74, 92, 55]

best_score  = max(scores)
best_index  = scores.index(best_score)
best_player = names[best_index]

print(f"Top player: {best_player} ({best_score})")
print(f"Average:    {sum(scores) / len(scores):.1f}")

ranked = sorted(scores, reverse=True)
print(f"Distribution (ranked): {ranked}")

for i in range(len(ranked)):
    print(f"  Rank {i + 1}: {ranked[i]}")

Demonstrating the difference between aliasing and copying, and between shallow and deep copies of nested lists.

python
# Aliasing: b is not a copy
a = [1, 2, 3]
b = a
b.append(4)
print(a)    # [1, 2, 3, 4]  (same object)

# Shallow copy: outer list is independent, inner lists are shared
matrix    = [[1, 2, 3], [4, 5, 6]]
shallow   = matrix.copy()
shallow[0].append(99)
print(matrix)    # [[1, 2, 3, 99], [4, 5, 6]]  (inner list shared)

# Manual deep copy with a for loop (no imports needed)
matrix    = [[1, 2, 3], [4, 5, 6]]
deep_copy = []
for row in matrix:
    deep_copy.append(row[:])    # copy each inner list explicitly

deep_copy[0].append(99)
print(matrix)    # [[1, 2, 3], [4, 5, 6]]  (unchanged)