Python tuples, explained
Tuples look like "immutable lists", but that framing misses the point. Tuples model fixed records — a related group of values with meaning by position — while lists model homogeneous collections. This guide covers the distinction, packing/unpacking, the depth of immutability, and named tuples.
Tuple vs list: it's about meaning
The technical difference is mutability, but the idiomatic difference is intent:
point = (3, 4) # a record: (x, y) — fixed structure
scores = [88, 92, 79] # a collection: any number of similar items
Use a tuple when position has meaning and the size is fixed (coordinates, a database row, a function returning multiple values). Use a list when you have a sequence of similar things you might add to or reorder.
Packing and unpacking
Creating a tuple is "packing"; pulling it apart is "unpacking". Parentheses are often optional:
p = 3, 4 # packing — p is (3, 4)
x, y = p # unpacking
x, y = y, x # swap with no temp variable
first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4] — star unpacking
The one gotcha: a single-element tuple needs a trailing comma, because the parentheses alone mean grouping:
(5) # int 5
(5,) # tuple (5,)
Tuples as return values
A function "returning multiple values" is really returning one tuple, which the caller unpacks:
def min_max(nums):
return min(nums), max(nums) # packs a tuple
lo, hi = min_max([3, 1, 7]) # unpacks it
Is immutability deep?
No — a tuple's immutability is shallow. You can't rebind which objects the tuple holds, but if one of those objects is mutable, it can still change:
t = (1, [2, 3])
t[1].append(4) # OK — the list inside is mutable -> (1, [2, 3, 4])
t[1] = [9] # TypeError — can't rebind a tuple slot
A consequence: a tuple is hashable only if all its elements are. (1, 2) is a valid
dict key; (1, [2]) is not.
Why a tuple can be a dict key
Because an all-immutable tuple is hashable and its hash never changes, it can be a dictionary key or set element — a list cannot:
distances = {(0, 0): 0, (3, 4): 5} # coordinates as keys — fine
{[0, 0]: 0} # TypeError: unhashable type: 'list'
namedtuple — fields with names
Plain tuples force you to remember what index means what. collections.namedtuple gives
the fields names while staying a tuple (and still immutable, lightweight, and unpackable):
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
p.x, p.y # 3, 4 — readable access
p[0] # 3 — still indexable like a tuple
x, y = p # still unpackable
p._replace(x=10) # Point(x=10, y=4) — returns a new tuple
typing.NamedTuple — the typed, class-based form
For type hints and a cleaner class syntax, use typing.NamedTuple:
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
def distance(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5
Point(3, 4).distance() # 5.0
When you need mutability or lots of behaviour, reach for a @dataclass instead; for a
small immutable record, a named tuple is perfect.
Recap
Tuples model fixed records; lists model collections — that intent matters more than
"immutable list". Pack with commas, unpack with assignment (and remember the trailing comma
for one-element tuples). Immutability is shallow: a tuple can contain a mutable list,
and a tuple is hashable (usable as a dict key) only when all its elements are. Reach for
collections.namedtuple or typing.NamedTuple to name the fields without giving up
tuple-ness.