Skip to content
teach

Lesson 3. Lists and Slicing

Mission link: The list is the default container in Python, and three of its behaviours produce silent bugs rather than errors. Those three are the whole lesson.
Primary source: The Python Tutorial, More on Lists
Prerequisites: Lesson 2

Warm-up

  1. ▢ row = (1, [2]); row[1].append(3). Legal or not, and why?
Check

Legal. The tuple fixes which objects it holds, and the list it holds is still mutable.

  1. ▢ What kind of copy is list(original), and when is that not enough?
Check

A shallow copy: a new list holding the same objects. Not enough when the elements themselves are mutable and have to be independent.

Know this

A list is a mutable, ordered sequence. Three of its behaviours are worth more than all its methods put together.

A slice builds a new list

items = [0, 1, 2, 3, 4]
part  = items[1:3]      # [1, 2], a new list
part.append(99)
print(items)            # [0, 1, 2, 3, 4], untouched

items[start:stop:step], with stop excluded. Every index is optional, so items[:] is the idiomatic shallow copy of a whole list. Slicing never fails on out-of-range bounds: items[2:99] gives you what is there, while items[99] raises IndexError. Negative indices count from the end.

The one to remember: a slice of a list is a copy, and indexing a list is not. items[1] hands you the object itself.

In-place methods return None

items = [3, 1, 2]
items.sort()                    # returns None, sorts items
print(items)                    # [1, 2, 3]

result = [3, 1, 2].sort()
print(result)                   # None

This is a deliberate convention across the language: a method that mutates the object returns None so that nothing looks like it produced a value. append, extend, insert, remove, reverse, sort and clear all return None.

Every one has a non-mutating counterpart when you need a new object instead:

In place, returns None Builds a new object
items.sort() sorted(items)
items.reverse() reversed(items), or items[::-1]
items.append(x) items + [x]
items.extend(other) items + other

The bug this convention prevents is items = items.sort(), which discards the list and leaves you with None. The bug it causes is writing that line anyway and being confused by the error two functions later.

+= mutates a list, + does not

a = [1]
b = a
a += [2]                # in place: calls list.__iadd__, extends the list
print(b)                # [1, 2], b sees it

a = [1]
b = a
a = a + [2]             # new list, then rebinding
print(b)                # [1], b did not move

For a list, += is extend, followed by rebinding the name to the same object it already referred to. Every other name for that list observes the change.

For an immutable object there is no in-place option, so += can only build a new object and rebind:

s = "a"
s += "b"                # new string, s rebound; nothing else could observe it

So += means "mutate if you can, otherwise rebuild". Whether other names see the result depends entirely on the type on the left, which is why lesson 1 called this the most common reason people think Python is inconsistent.

Multiplication shares, it does not duplicate

grid = [[0] * 3] * 2        # two names for ONE inner list
grid[0][0] = 9
print(grid)                 # [[9, 0, 0], [9, 0, 0]]

* 2 repeats the reference twice. The fix is a comprehension, which evaluates its expression once per item:

grid = [[0] * 3 for _ in range(2)]

[0] * 3 is fine, because 0 is immutable and sharing it is unobservable.

Practice

  1. ▢ Predict the output.

    names = ["a", "b", "c"]
    first_two = names[:2]
    first_two[0] = "z"
    print(names, first_two)
    
Check

['a', 'b', 'c'] ['z', 'b'].

The slice built a new list. Assigning into first_two rebinds one of its slots, and names has no idea.

  1. ▢ Predict both prints. They are not the same.

    a = [1]
    b = a
    a += [2]
    print(b)
    a = a + [3]
    print(b)
    
Hint

One of these two operations gives the list a chance to change itself. The other has to build something new before the assignment happens.

Check

[1, 2], then [1, 2].

+= extended the shared list in place, so b saw the 2. Then a + [3] built a new list and rebound a to it, leaving b on the old object, which still ends at 2.

  1. ▢ What does this print, and what did the author intend to write?

    scores = [5, 2, 9]
    scores = scores.sort()
    print(scores)
    
Check

None.

sort mutates in place and returns None by convention, and the assignment threw the list away. The author wanted either scores.sort() with no assignment, or scores = sorted(scores).

  1. ▢ Which line makes a grid where the rows are independent?

    • a) grid = [[0] * 3] * 2
    • b) grid = [[0] for _ in "ab"] * 2
    • c) grid = [[0] * 3 for _ in "ab"]
    • d) grid = list([[0] * 3] * 2)
Check

c) grid = [[0] * 3 for _ in "ab"].

The comprehension evaluates [0] * 3 once per iteration, so each row is a separate list. Option a repeats one reference. Option b builds two independent rows and then repeats both references, giving four entries and two distinct lists. Option d is a shallow copy of the broken structure from option a, so the rows are still shared.

  1. ▢ You are reviewing a function that takes items: list and starts with items = items[:]. What is the author protecting against, and what are they not protecting against?
Check

They are protecting the caller from any mutation of the list itself: appends, removals, sorting, slot assignment. After that line, the function's name refers to a different list, so the caller's list cannot be changed.

They are not protecting the caller from mutation of the elements. If the list holds dictionaries and the function modifies one, the caller sees it, because the shallow copy holds the same dictionaries.

Real-world reps

  • [ ] Run the += and + pair from the lesson, and check id(a) before and after each. The identity is the evidence, and it makes the difference impossible to forget.
  • [ ] Build [[0] * 3] * 2, mutate one cell, and look at the result. Then fix it with a comprehension.
  • [ ] Tomorrow: search code you know for = .*\.sort\(\) or = .*\.append\(. Any hit is a bug or about to be one.

Going further


Not landing? Reread the primary source at the top, since this lesson compresses it and compression is where understanding leaks. Check the glossary for any term that felt slippery.

If the lesson itself is unclear rather than the material, that is a defect: open an issue.

Table of contents