Lists and Tuples
Lists and Tuples
A list is a growable sequence. A tuple is a fixed sequence. The boring default is a list when the desk will append, sort, or drop rows, and a tuple when the values are a record (id, table, status) that should not grow a fourth surprise field by accident.
Mental model
Both are ordered, both index from 0, both slice with items[start:stop]. The difference is mutability. tickets.append(t) changes the list. There is no tuple.append. To “change” a tuple you build a new one.
Names are not copies. b = a makes two names for one list. a[:] and list(a) make a shallow copy: a new outer list, same inner objects. Nested lists still alias. copy.deepcopy copies the tree.
A slice of a list is a new list. A slice of a tuple is a new tuple. Either way, the elements are the same objects.
A tuple as a record is positional: ticket[0] is easy to mix up with ticket[1]. Unpack it (id, table, status = ticket) or move to a dataclass when the record grows names.
Worked examples
Case 1: Lists change, tuples do not
Save as list_vs_tuple.py.
# list_vs_tuple.py
def main():
tickets = [7, 8]
tickets.append(9)
tickets[0] = 70
print(tickets)
row = (7, 12, "open")
print(row)
print(row[0], row[-1])
try:
row[0] = 70
except TypeError as e:
print(e)
if __name__ == "__main__":
main()Run:
uv run python list_vs_tuple.pyOutput:
[70, 8, 9]
(7, 12, 'open')
7 open
'tuple' object does not support item assignment
row[-1] is the last field. Use it. Do not mutate a tuple; you cannot.
Case 2: Alias versus copy
# alias_copy.py
def main():
a = [7, 8, 9]
alias = a
copied = a[:]
alias.append(10)
copied.append(99)
print("a ", a)
print("alias ", alias)
print("copied", copied)
print("alias is a", alias is a)
print("copied is a", copied is a)
if __name__ == "__main__":
main()Run:
uv run python alias_copy.pyOutput:
a [7, 8, 9, 10]
alias [7, 8, 9, 10]
copied [7, 8, 9, 99]
alias is a True
copied is a False
alias.append changed a. The slice did not. When a function does tickets.append(...), every name that points at that list sees the append.
Case 3: Shallow copy and nested lists
# shallow.py
import copy
def main():
shift = [[7, 8], [9]]
shallow = shift[:]
deep = copy.deepcopy(shift)
shallow[0].append(70)
print("shift ", shift)
print("shallow", shallow)
print("deep ", deep)
if __name__ == "__main__":
main()Run:
uv run python shallow.pyOutput:
shift [[7, 8, 70], [9]]
shallow [[7, 8, 70], [9]]
deep [[7, 8], [9]]
shift[:] copied the outer list only. shallow[0] is the same inner list as shift[0]. deepcopy made new inner lists. Use deepcopy when you have lists of lists (or lists of dicts) and you need a snapshot.
Case 4: Tuple as a record
# ticket_row.py
def label(row):
ticket_id, table, status = row
return f"ticket {ticket_id} table {table} ({status})"
def main():
open_row = (7, 12, "open")
paid_row = (8, 3, "paid")
print(label(open_row))
print(label(paid_row))
print(open_row + (False,))
if __name__ == "__main__":
main()Run:
uv run python ticket_row.pyOutput:
ticket 7 table 12 (open)
ticket 8 table 3 (paid)
(7, 12, 'open', False)
Unpacking names the fields for the length of the function. + on tuples concatenates and returns a new tuple; open_row is unchanged. A fourth anonymous False is why records outgrow tuples — you cannot tell what False is. That is the cue for a dataclass.
The trap
Slicing looks like a copy of everything. It is not, once values are mutable.
# slice_alias.py
def main():
tickets = [{"id": 7, "status": "open"}, {"id": 8, "status": "paid"}]
window = tickets[:1]
window[0]["status"] = "void"
print("window ", window)
print("tickets", tickets)
if __name__ == "__main__":
main()Run:
uv run python slice_alias.pyOutput:
window [{'id': 7, 'status': 'void'}]
tickets [{'id': 7, 'status': 'void'}, {'id': 8, 'status': 'paid'}]
window is a new list of length 1. The dict inside is the same dict as tickets[0]. Changing status through either name changes both. Copy the dict (dict(t) or {**t}) or deepcopy the structure when the window must be independent.
The boring rule
- List: variable length, same kind of thing (tickets, tables, notes).
- Tuple: fixed record or a return of two or three values. Unpack it.
b = aaliases.a[:]/list(a)shallow-copy. Nested mutables still shared.- Do not assign through a slice you thought was a snapshot.
- When a tuple grows named fields, use a dataclass.
Try this
- In
alias_copy.py, uselist(a)instead ofa[:]. Confirmcopied is ais stillFalse. - In
ticket_row.py, unpack with a wrong number of names (id, table = open_row) and read theValueError. - Fix
slice_alias.pyso the window holds{**tickets[0]}(a new dict). Void the window copy only.