What is the difference between a list and a tuple, and when should you use each?
Lists are mutable sequences; tuples are immutable. Use a tuple when the collection of items is fixed by meaning — coordinates, RGB values, function return values — and a list when the collection will grow, shrink, or be modified in place. Immutability also makes tuples hashable, so they can serve as dict keys or set members.
How to think about it
The surface answer — “lists are mutable, tuples are immutable” — is true and incomplete. A stronger answer says why that matters in practice: an immutable tuple is hashable, so it can be a dict key or set member; it’s slightly leaner in memory; and, just as importantly, it carries meaning — a tuple tells the next developer “these values belong together and won’t change,” which a list never promises.
The dividing line is mutability:
coords = (40.7128, -74.0060) # tuple — a fixed (lat, lon) pair
coords[0] = 0.0 # TypeError: 'tuple' object does not support item assignment
names = ["Alice", "Bob"]
names.append("Carol") # fine — a list is meant to change
And because a tuple’s slots can’t be reassigned, Python can give it a stable hash — which is what lets it be a key where a list simply can’t:
grid = {}
grid[(0, 0)] = "origin" # works — a tuple is hashable
grid[[0, 0]] = "origin" # TypeError: unhashable type: 'list'
That single ability — tuple-as-key — quietly powers grid coordinates, composite cache keys, and multi-column lookups.
A worked example
import sys
# Tuples are a little leaner (lists over-allocate to make append cheap)
t = (1, 2, 3)
l = [1, 2, 3]
print(f"tuple size: {sys.getsizeof(t)} bytes")
print(f"list size: {sys.getsizeof(l)} bytes")
# Hashable -> tuples make natural composite keys
locations = {
(40.71, -74.00): "New York",
(51.50, -0.12): "London",
(35.68, 139.69): "Tokyo",
}
print("NYC:", locations[(40.71, -74.00)])
# Tuples are the idiomatic "return several values" container
def bounding_box(points):
xs = [p[0] for p in points]
ys = [p[1] for p in points]
return (min(xs), min(ys), max(xs), max(ys))
x_min, y_min, x_max, y_max = bounding_box([(1, 2), (5, 3), (0, 8)])
print(f"Bounding box: ({x_min},{y_min}) to ({x_max},{y_max})")
# The shallow-immutability gotcha
t2 = ([1, 2], 3)
t2[0].append(99) # the list INSIDE the tuple can still change
print("Mutated inner list:", t2)
try:
hash(([1, 2], 3)) # ...so the tuple can't be hashed
except TypeError as e:
print("Hash error:", e)
tuple size: 64 bytes
list size: 120 bytes
NYC: New York
Bounding box: (0,2) to (5,8)
Mutated inner list: ([1, 2, 99], 3)
Hash error: unhashable type: 'list'
The sizes tell a small story: the list is nearly twice the tuple, because a list keeps spare capacity so that append is usually free, while a tuple — which will never grow — allocates exactly what it needs.
When to use which
| Situation | Choose |
|---|---|
| Fixed-meaning record (lat/lon, RGB, DB row) | tuple |
| Returning several values from a function | tuple |
| A collection that grows or gets filtered | list |
| A stack, queue, or pipeline of items | list |
| A dict key or set member | tuple |
For large numerical data, by the way, neither is the right tool — reach for a NumPy array.