Execute Program

Python for Programmers: Mutable List Problems

Welcome to the Mutable List Problems lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • Like many languages, Python sometimes uses mathematical operators for operations that don't involve numbers. For example, we've already used + to combine strings.

  • >
    "left a" + "nd right"
    Result:
    'left and right'Pass Icon
  • We've also used + to concatenate lists.

  • >
    first_primes = [2, 3, 5]
    more_primes = [7, 11, 13]
    first_primes + more_primes
    Result:
    [2, 3, 5, 7, 11, 13]Pass Icon
  • Strings and lists also support the * operator, which is fairly uncommon in programming languages. If we "multiply" a string by a number, we get that string repeated that number of times. For example, 3 * some_string gives us the same result as some_string + some_string + some_string.

  • >
    3 * "more"
    Result:
    'moremoremore'Pass Icon
  • With numbers, x * y = y * x. The same is true with strings: we get the same result if we switch the operands' order.

  • >
    "more" * 3
    Result:
    'moremoremore'Pass Icon
  • This is mostly a novelty, but does have its uses. For example, it can be useful when building string output in command-line tools. The next example builds up an ASCII-art arrow of a specified length. We can use this to dynamically size arrows, allowing us to style our tool's output.

  • >
    arrow_length = 5

    arrow = "=" * (arrow_length - 1) + ">"
    arrow
    Result:
    '====>'Pass Icon
  • The same trick works with lists: * gives us a new list, with the original list's contents repeated a certain number of times.

  • >
    [1, 2] * 3
    Result:
    [1, 2, 1, 2, 1, 2]Pass Icon
  • At first, this seems much more useful, since it lets us initialize a list of a certain size. The next example creates a list of four zeros.

  • >
    [0] * 4
    Result:
    [0, 0, 0, 0]Pass Icon
  • That code works and is safe. However, this technique becomes very dangerous in some situations. To see why, we'll switch to creating lists of lists.

  • In the next example, we make a list of 3 empty lists. They're 3 distinct empty lists, each stored at a different location in memory. When we modify one list, the other two lists are unaffected.

  • >
    three_lists = [[], [], []]
    three_lists[0].append(1)
    three_lists[1]
    Result:
    []Pass Icon
  • What if we extract the empty list into its own variable, then reference the variable three times? Now, changing three_lists[0] also changes three_lists[1] and three_lists[2]. They're all the same list!

  • >
    a_list = []
    three_lists = [a_list, a_list, a_list]
    three_lists[0].append(1)
    three_lists
    Result:
  • The problem is that while three_lists has three elements, each element references to the same underlying list, stored at the same location in memory.

  • Now back to the * operator: some_list * 3 is like the last example above. If some_list contains another list, "multiplying" it by 3 creates 3 references to the same inner list. Modifying any element affects the other elements.

  • >
    some_list = [[]]
    three_lists = some_list * 3
    three_lists[0].append(1)
    three_lists
    Result:
    [[1], [1], [1]]Pass Icon
  • >
    some_list = [[]]
    three_lists = some_list * 3
    three_lists[0].append(1)
    three_lists[1].append(2)
    three_lists[2].append(3)
    three_lists
    Result:
    [[1, 2, 3], [1, 2, 3], [1, 2, 3]]Pass Icon
  • The same problem happens any time we use * on a list that contains mutable values (values that can change over time). For example, lists of dicts have the same problem.

  • When we create a list of dicts, but don't modify them, the problem doesn't show up.

  • >
    some_list = [{
    "age": 36
    }]
    three_dicts = some_list * 3
    three_dicts
    Result:
  • But when we modify three_dicts[0], the change shows up in three_dicts[1] and three_dicts[2].

  • >
    some_list = [{
    "age": 36
    }]
    three_dicts = some_list * 3
    three_dicts[0]["age"] += 1
    three_dicts
    Result:
  • This issue isn't specific to *, as we saw in some examples earlier in this lesson, where we did [a_list, a_list, a_list]. However, Python code often uses some_list * some_length to initialize a list to a fixed size. It's easy to introduce this kind of problem when doing that.

  • This problem also isn't specific to Python. It's a fundamental problem in imperative programming languages, and most programming languages are imperative!