Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lists

Lists are ordered, mutable sequences. Use them when position matters or when you want to keep adding and removing items over time.

Think of a list as a numbered row of slots. Slot 0 is the first item, slot 1 is the second, and so on. A list holds one element type, so a list[str] is for strings only.

Creating lists

fruits = ["apple", "banana", "orange"]
tasks: list[str] = []
numbers: list[int] = [1, 2, 3]

Use a literal when you already have values. Use an empty list when you plan to fill it later. If the compiler cannot infer the element type from context, add an annotation.

Lists are dynamic arrays. Appending is cheap most of the time because storage grows in chunks, not one item at a time.

Reading values

items = ["first", "second", "third", "fourth"]

print(items[0])
print(items[-1])
print(items[1:3])
print(len(items))
print("second" in items)
print(items.index("third"))

Indexing starts at 0. Negative indexes count from the end. Slices use the familiar start:stop form and include the start but exclude the stop.

If you are coming from a one-based indexing language, the first item is still at position 0 here.

Updating lists

items = ["first", "second", "third"]

items.append("fourth")
items.insert(1, "new")
items.extend(["fifth", "sixth"])

print(items)
print(items.pop())
print(items.pop(0))
del items[1]

append() adds one item to the end. insert() places an item at a specific index. extend() adds several items from another iterable. pop() removes and returns an item, and del items[i] removes by index without returning anything.

Appends are amortized O(1). Inserting or deleting near the front of a long list is O(n) because elements need to shift. `pop()` is O(1) at the end and O(n) at other positions.

Joining and repeating lists

a = [1, 2]
b = [3]

print(a + b)    # [1, 2, 3]
print(a * 2)    # [1, 2, 1, 2]

+ and * return a new list and do not change their operands.

The augmented forms += and *= change the list on the left in place, as in Python. Every name that refers to that list sees the change:

items = ["a"]
alias = items
items += ["b", "c"]
items *= 2
print(alias)    # ['a', 'b', 'c', 'a', 'b', 'c']

items += more copies the elements of more, like items.extend(more), so its cost does not grow with the length of items. items *= n writes only the added copies.

Common utilities

values = [9, 5, 123, 14, 1, 5]

print(sorted(values))
values.reverse()
print(values)
print(values.count(5))

copy_of_values = values.copy()
values.clear()
print(copy_of_values)
print(values)

sorted() returns a new list. reverse() changes the existing list in place. count() scans the whole list and counts matches. copy() makes a shallow copy, which is enough when the elements themselves are simple values.

If the list contains mutable values, a shallow copy only duplicates the outer list. The items inside are still shared.

Iterating over lists

names = ["Ada", "Bjarne", "Grace"]

for name in names:
    print(name)

for i, name in enumerate(names):
    print(i, name)

Iteration gives you each item in order. Use enumerate() when you also need the current index.

List comprehensions

numbers = [1, 2, 3, 4, 5]
squares = [n * n for n in numbers]
evens = [n for n in numbers if n % 2 == 0]

List comprehensions are the compact way to build a new list from an existing iterable. Read them as "make a list of this expression for each item that matches the condition".

Generator expressions

A generator expression uses parentheses to produce an Iterator[A]. It computes values as they are requested, while a list comprehension builds the whole list immediately.

numbers = [1, 2, 3, 4, 5]
squares = (n * n for n in numbers if n % 2 == 0)

print(list(squares))    # [4, 16]
print(list(squares))    # []

Generators are single-pass: a for loop or a consumer such as list() or sum() advances the iterator. Once exhausted, it produces no more values. Construct another generator if you need to repeat the traversal.

When a generator is the only argument to a call, its extra parentheses can be omitted. Keep them when passing other arguments:

print(sum(n * n for n in numbers))         # 55
print(sum((n * n for n in numbers), 10))    # 65

Like comprehensions, generator expressions can have multiple for and if clauses. Clauses run from left to right, with each inner loop traversed for the current outer value:

pairs = ((x, y) for x in range(2) for y in range(3) if x != y)
print(list(pairs))    # [(0, 1), (0, 2), (1, 0), (1, 2)]

Evaluation and captures

The outermost source expression is evaluated and converted to an iterator when the generator is constructed. Filters, nested source expressions, and the result expression are evaluated as iteration advances. Errors in the outer source therefore occur at construction; errors in deferred expressions occur when those expressions are reached during iteration.

The deferred expressions must be pure. They can compute values and raise exceptions, but cannot call functions requiring mut, proc, or action effects. Use an explicit for loop when each step needs those effects.

Local variables and actor state referenced from the surrounding scope are captured by value when the generator is constructed, as with Acton lambdas:

factor = 2
scaled = (factor * n for n in [1, 2, 3])
factor = 3
print(list(scaled))    # [2, 4, 6]

Capturing a mutable object keeps a reference to that object; it does not copy its contents. A generator does not take a snapshot of its source collection.

The compiler can combine generator production and consumption into a single loop, avoiding an intermediate iterator pipeline while preserving these evaluation rules.

Flattening with flatmap

flatmap(f, items) returns a lazy iterator that calls f for each input and yields all values from the returned iterator before moving to the next input. Empty inner iterators contribute no values.

groups = [[1, 2], [], [3]]
flattened = flatmap(lambda group: iter(group), groups)
print(list(flattened))    # [1, 2, 3]

The input can be any Iterable[A]. The callback must be pure and return an Iterator[B]; wrap collections in iter() inside the callback. The result is a single-pass Iterator[B], just like a generator expression with nested for clauses.

Type safety

All items in a list must be of the same type. Mixing types like ["foo", 1, True] will not compile.

strings = ["foo", "bar", "baz"]
numbers = [1, 2, 3, 4, 5]

When a list is empty, give it a type if the surrounding code does not make the element type obvious.