A list is an ordered, mutable sequence
A list holds references to objects of any type, grows on demand, and is indexed and sliced exactly like a string. Everything about s[a:b:c] from the strings lesson applies here too.
items = ["a", "b", "c"]
items.append("d") # at the end — O(1)
items.insert(0, "z") # at a position — O(n), everything after shifts
items.extend(["e", "f"]) # several at once ( items += [...] is the same )
items.remove("b") # the first one equal to "b"; ValueError if absent
last = items.pop() # take from the end — O(1)
first = items.pop(0) # take from the front — O(n); use collections.deque instead
items[1:3] = ["x"] # slices assign, and can change the length
Two names, one list
Assignment never copies in Python. It binds a name to an object, and both names then see the same object — which is the single most common source of "but I never changed that" bugs.
A shallow copy makes a new list containing the same inner objects: change one of those inner objects and both lists see it. copy.deepcopy copies all the way down, and is slow enough that you should only reach for it when you mean it.
a = [1, 2, 3]
b = a # NOT a copy — another name for the same list
b.append(4)
print(a) # [1, 2, 3, 4]
c = a.copy() # a shallow copy — also list(a), or a[:]
c.append(5)
print(a) # unchanged
import copy
d = copy.deepcopy(nested) # when the list contains lists
The multiplication trap
[x] n repeats the reference n times. For immutable contents ([0] 3) that is harmless; for a list of lists it gives you one row pretending to be three.
grid = [[0] * 3] * 3 # three references to ONE row
grid[0][0] = 9
print(grid) # [[9, 0, 0], [9, 0, 0], [9, 0, 0]]
grid = [[0] * 3 for _ in range(3)] # three separate rows
grid[0][0] = 9
print(grid) # [[9, 0, 0], [0, 0, 0], [0, 0, 0]]
Sorting
list.sort() sorts in place and returns None — items = items.sort() is a bug that assigns nothing, and it happens to everyone once. sorted() returns the new list.
Python’s sort is stable: equal keys keep their previous order. That is what makes sorting twice work — sort by the secondary key first, then by the primary.
people = [("ada", 36), ("grace", 45), ("alan", 41)]
people.sort() # in place, returns None
ordered = sorted(people) # a new list, leaves the original alone
sorted(people, key=lambda p: p[1]) # by age
sorted(people, key=lambda p: -p[1]) # descending, numerically
sorted(people, key=lambda p: (p[1], p[0])) # by age, then by name
sorted(words, key=str.casefold) # case-insensitive
max(people, key=lambda p: p[1]) # the same key idea
Testing membership
x in items scans the list: it is O(n). For a big collection you are looking things up in repeatedly, convert it to a set once and the test becomes O(1). That one change has rescued more slow scripts than any other.
banned = {"a", "b", "c"} # a set literal
[w for w in words if w not in banned]