CSC108H1/Unit 5
Unit 5 · Intro to Programming

Lists & Aliasing

Lists index like strings but, unlike strings, they can be changed in place. That one difference creates the hardest concept in the course: two names can refer to the same list, so changing it through one name changes what the other sees. Almost every mysterious bug in CSC108 is this.

By the end you can
  • Distinguish mutating a list from rebinding a name.
  • Predict when two names share a list and when they do not.
  • Copy a list correctly, including the nested case.
  • Explain why a function can change its caller's list.
Why it matters & where it comes from
Why it exists

Mutation is efficient: appending to a list of a million items does not copy a million items. The cost is that a list is a shared thing, and sharing is invisible in the source unless you are watching for it.

In the real world

Shared mutable state is the root of most concurrency bugs in every language. The discipline this unit teaches — know who else can see this object — is the same discipline that keeps large systems correct.

Story & open questions

Whole languages exist to avoid this. Haskell and Clojure make data immutable by default, so aliasing cannot bite; the trade is that every change copies. Python picks the other side, which is why you have to think about it.

Lists are mutable

Definition

A list is an ordered, mutable sequence written [1, 2, 3]. It indexes and slices exactly like a string, but item assignment is allowed: xs[0] = 99 works.

Example The mutating operations
python
xs = [3, 1, 2]

xs.append(4)      # [3, 1, 2, 4]   - adds one item at the end
xs.extend([5, 6]) # [3, 1, 2, 4, 5, 6] - adds each item of another list
xs.insert(0, 0)   # [0, 3, 1, 2, 4, 5, 6]
xs.remove(3)      # removes the FIRST 3 by value
xs.pop()          # removes and returns the last item
xs.sort()         # sorts in place, returns None
xs.reverse()      # reverses in place, returns None

Every one of these changes xs itself. Contrast the string methods of Unit 4, which changed nothing.

In-place methods return None

xs = xs.sort() sets xs to None and loses the list. The sort already happened; there is nothing to assign. If you want a sorted copy, use the built-in sorted(xs), which returns a new list and leaves the original alone. Same pairing for reverse() and reversed().

Aliasing

Rule Assignment never copies

ys = xs binds a second name to the same list. There is now one list with two names. Mutating through either name is visible through both.

Example Mutate versus rebind
python
xs = [1, 2, 3]
ys = xs

ys.append(4)     # MUTATES the shared list
print(xs)        # [1, 2, 3, 4]  - xs sees it

ys = [9]         # REBINDS ys to a different list
print(xs)        # [1, 2, 3, 4]  - xs unaffected
The distinction is everything. A method call like append reaches into the object. An assignment to the bare name moves the label somewhere else.
Check with xs is ys, which asks whether they are the same object — different from xs == ys, which asks whether the contents match.
Example == versus is
python
a = [1, 2]
b = [1, 2]
c = a

a == b   # True  - same contents
a is b   # False - two separate lists
a is c   # True  - one list, two names

Copying

Rule Three ways to copy, one gap
ys = xs[:] or ys = list(xs) makes a shallow copy: a new outer list holding the same inner objects.
For a flat list of numbers or strings, shallow is enough.
For a list of lists it is not: the inner lists are still shared.
Example Where shallow fails
python
grid = [[0, 0], [0, 0]]
copy = grid[:]          # new outer list, SAME two inner lists

copy[0][0] = 9
print(grid)             # [[9, 0], [0, 0]]  - leaked through

copy.append([1, 1])
print(len(grid))        # 2 - the outer list really was copied

To copy properly, copy each row: copy = [row[:] for row in grid].

The multiplication trap

grid = [[0] * 3] * 2 looks like a 2-by-3 grid and is a disaster: it makes one inner list and points at it twice. Setting grid[0][0] = 1 changes both rows. Build it with [[0] * 3 for _ in range(2)], which evaluates the inner list fresh each time.

Lists as arguments

Rule The parameter is an alias

Passing a list to a function binds the parameter to the same list. The function can mutate the caller's data. This is not a leak — it is how sort works — but it must be deliberate and documented.

Example Two contracts, clearly stated
python
def add_one_bad(xs: list) -> None:
    """Add 1 to every item of xs, modifying xs in place."""
    for i in range(len(xs)):
        xs[i] = xs[i] + 1

def add_one(xs: list) -> list:
    """Return a new list with 1 added to every item of xs.

    >>> add_one([1, 2])
    [2, 3]
    """
    return [x + 1 for x in xs]
Neither is wrong, but the docstrings must say which one it is. A function that mutates and also returns something is the worst of both and confuses every caller.
Note the first returns None, matching the convention of sort and append: in-place operations return nothing.
Example Rebinding does not escape
python
def wipe(xs: list) -> None:
    xs = []          # rebinds the LOCAL name only

data = [1, 2, 3]
wipe(data)
print(data)          # [1, 2, 3] - untouched

To actually empty the caller's list you must mutate it: xs.clear() or del xs[:]. Same rule as always — assignment moves a label, it does not touch the object.

Common pitfalls

• xs = xs.sort(). In-place methods return None.
• Believing ys = xs copies. It never does.
• Shallow-copying a nested list and being surprised when inner changes leak.
• [[0] * 3] * 2 for a grid. Use a comprehension.
• Removing from a list while iterating it, as in Unit 2.

Practice

Predict each answer before running it. Original exercises written for this guide in the style of the course's weekly sets.

Practice 1 · Trace

What prints?

python
a = [1, 2]
b = a
b.append(3)
b = [7]
print(a)
Answer
[1, 2, 3]. The append mutated the shared list; the later assignment only moved b.
Practice 2 · sort returns

What is x?

python
x = [3, 1, 2].sort()
Answer
None. The list was sorted and then discarded, since nothing else referred to it. Use sorted([3, 1, 2]).
Practice 3 · Nested copy

Copy g = [[1, 2], [3, 4]] so changing the copy cannot affect g.

Solution
c = [row[:] for row in g]. A plain g[:] shares the inner lists.
Practice 4 · Grid trap

What does this print, and why?

python
g = [[0] * 2] * 2
g[0][0] = 5
print(g)
Answer
[[5, 0], [5, 0]]. There is only one inner list, referenced twice, so the change appears in both rows.
Practice 5 · is versus ==

When is a == b True but a is b False?

Answer
Whenever the contents match but the objects are distinct, as with two separately built [1, 2] lists. Use == for values; is only for identity, and in practice mostly for is None.
Practice 6 · Write it

Write a function returning a new list with duplicates removed, order preserved.

Solution
python
def dedupe(xs: list) -> list:
    """Return a new list with duplicates of xs removed, keeping first
    occurrences in order.

    >>> dedupe([1, 2, 1, 3, 2])
    [1, 2, 3]
    """
    out = []
    for x in xs:
        if x not in out:
            out.append(x)
    return out
Unit 6 shows a faster version using a set, since x not in out scans the whole list each time.
Practice 7 · Mutating parameter

Does this change the caller's list?

python
def f(xs: list) -> None:
    xs = xs + [1]
Answer
No. xs + [1] builds a new list and the assignment rebinds only the local name. xs.append(1) would change the caller's.
Practice 8 · Flatten

Flatten [[1, 2], [3], []] to [1, 2, 3].

Solution
python
def flatten(rows: list) -> list:
    """Return one list containing every item of every row.

    >>> flatten([[1, 2], [3], []])
    [1, 2, 3]
    """
    out = []
    for row in rows:
        out.extend(row)
    return out
extend adds each item; append would add the row itself as a single nested element.

Expanded topic checklist

Course coverage. This unit is part of the complete CSC108H1 topic map. It should cover the following items before you move on:

  • Mutation. The in-place methods and the fact they return None.
  • Aliasing. Assignment binds names; is versus ==.
  • Copying. Shallow copy, and the nested case that needs more.
  • Parameters. Why a function can change its caller's list, and how to document which it does.
Textbook/source alignment

The calendar lists "lists" among the elementary data types. Aliasing is the concept the term test reliably examines through tracing questions; check the syllabus for the assigned exercises.

Core idea map

Names refer to objects, from Unit 1. Lists are mutable, so several names can watch one object change. Copying is the deliberate act of breaking that sharing.

Study routine. Draw boxes and arrows. Names are labels with arrows to objects; mutation changes the object, assignment moves an arrow. Almost every tracing question falls out of one such diagram.

Deep practice extensions

Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.

Extension 1 · Aliasing

What does ys = xs do for a list?

Guide
Binds a second name to the same list. No copy is made.
Extension 2 · In-place return

What does xs.sort() return?

Guide
None. Use sorted(xs) when you want a new sorted list.
Extension 3 · Depth of copy

When is xs[:] not enough?

Guide
When the items are themselves mutable, such as a list of lists. The inner objects stay shared.
Extension 4 · Escaping a function

Which changes inside a function are visible to the caller?

Guide
Mutations of the passed object. Rebinding the parameter name is purely local.