Python by Example

Lists

Python list basics: append, extend, slicing, shallow copies, and the shared-reference trap with [[]] * n.

A Python list is a mutable, ordered sequence of items. You create one with square brackets ([1, 2, 3]) or by calling list(iterable). Lists grow with .append() and .extend(), shrink with .pop() and .remove(), and can be sliced with xs[start:stop].

The most common list operations: .append(x) adds one item to the end, .extend(iterable) appends every item from another iterable, .insert(i, x) inserts at a specific index, .pop() removes and returns the last item, and .remove(x) removes the first occurrence of a value. Appending and popping at the right end (index -1) is O(1) - fast regardless of list size. Inserting or removing at the left end (index 0) is O(n) because every other item must shift.

xs = [1, 2, 3]
 
xs.append(4)        # [1, 2, 3, 4]
xs.extend([5, 6])   # [1, 2, 3, 4, 5, 6]
xs.insert(0, 0)     # [0, 1, 2, 3, 4, 5, 6]  -- O(n), shifts everything
xs.pop()            # returns 6, list is [0, 1, 2, 3, 4, 5]
xs.remove(0)        # removes first 0, list is [1, 2, 3, 4, 5]
 
len(xs)             # 5
3 in xs             # True
xs.index(3)         # 2  (position of the value 3)

Slicing returns a new list containing the selected items. xs[a:b] gives items from index a up to (but not including) b. Negative indices count from the end: xs[-1] is the last item. xs[::-1] steps backwards and returns a reversed copy. xs[:] copies the entire list shallowly - useful when you need a separate list that starts with the same values.

xs = [10, 20, 30, 40, 50]
 
xs[1:3]     # [20, 30]          -- items at index 1 and 2
xs[:2]      # [10, 20]          -- first two
xs[2:]      # [30, 40, 50]      -- from index 2 to end
xs[-1]      # 50                -- last item
xs[::-1]    # [50, 40, 30, 20, 10]  -- reversed copy
xs[:]       # [10, 20, 30, 40, 50]  -- shallow copy
 
copy = xs[:]
copy.append(99)
xs   # [10, 20, 30, 40, 50]  -- original unchanged

[[]] * n looks like it creates n independent empty lists, but it actually creates n references to the same inner list. Appending to any one of them mutates all of them. The fix is a list comprehension: [[] for _ in range(n)] calls [] once per iteration, producing n distinct list objects.

# Bug: all three rows point to the same list
grid = [[]] * 3
grid[0].append(1)
grid  # [[1], [1], [1]]  -- all three changed!
 
# Fix: comprehension creates a new [] each iteration
grid = [[] for _ in range(3)]
grid[0].append(1)
grid  # [[1], [], []]  -- only row 0 changed
 
# Same trap with any mutable default
row = [0] * 5     # [0, 0, 0, 0, 0]  -- safe: ints are immutable
matrix = [[0] * 5 for _ in range(3)]  # correct 3x5 grid

In production

[[]] * n creates n references to the same inner list - appending to one mutates all of them. Use [[] for _ in range(n)] instead. Lists are O(1) for .append() and .pop() at the right end but O(n) for .pop(0) and .insert(0, x) because every item must shift; for FIFO queues reach for collections.deque, which is O(1) at both ends.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free