Understanding Tuples

A tuple is an ordered, immutable sequence. Syntactically it is written with commas — parentheses are optional — so point = 3, 4 and point = (3, 4) produce the same value. Once created a tuple cannot be changed: you cannot append, remove or replace items. This restriction is not a limitation; it is a feature that guarantees a value will not silently change, which in turn lets tuples be used as dictionary keys and set elements.

Tuples are the natural shape for a fixed-size record where each position has a meaning. A GPS coordinate is (latitude, longitude); an RGB color is (red, green, blue); a database row is (id, name, created_at). You can unpack a tuple back into named variables in a single line: lat, lon = coordinate. That one-line unpacking is why functions returning tuples feel natural to call.

There are two tuple footguns worth knowing up front. First, a single-item tuple needs a trailing comma: (42,) is a tuple, (42) is just the integer in parentheses. Second, while the tuple itself is immutable, the objects it references might not be: t = ([1, 2], 3) is a tuple whose first element is a list you can still t[0].append(4). Immutability is shallow.

Because tuples cannot be modified they can be hashed as long as every element is hashable. That makes them ideal composite dictionary keys: cache[(user_id, date)] = result. It also means tuples are measurably faster to create and iterate than lists of the same size, which is why functions in hot loops often return tuples instead of lists.

Creating, unpacking and indexing

You can create a tuple from any iterable with tuple(iter). Index and slice exactly like a list: t[0], t[-1], t[1:3]. Extended unpacking with * handles uneven splits: first, *rest, last = t assigns the middle region to a list.

Named tuples from collections.namedtuple or typing.NamedTuple keep the immutability but add field names, turning point.x and point.y into readable alternatives to point[0] and point[1].

When to prefer tuples over lists

Choose a tuple when the collection is a heterogeneous, fixed-length record (“one of each”) and choose a list when the collection is a homogeneous, variable-length sequence (“more of the same”). A row from a CSV is a tuple; the whole CSV is a list of tuples.

Also prefer tuples for default argument values. def f(seen=()) avoids the classic “mutable default argument” bug because the empty tuple can never be changed.

The core tuple operations.

ToolPurpose
tuple
built-in type
Immutable ordered sequence.
a, b = t
unpacking
Binds positions of t to names a and b.
a, *rest = t
extended unpacking
Captures the leftover positions as a list.
collections.namedtuple
factory
Named tuple with attribute access by name.
typing.NamedTuple
class
Typed, hashable immutable record.
hash(obj)
built-in
Returns a hash; raises for tuples containing unhashable items.
t + (x,)
operator
Returns a new tuple with x appended (creates a copy).
len(t)
built-in
Number of items in the tuple.

Understanding Tuples code example

The script below shows tuple creation, unpacking, hashability, and the shallow-immutability trap.

# Lesson: Understanding Tuples
from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])

empty = ()
single = (42,)
pair = (3, 4)
named = Point(3, 4)
mixed = ("alice", 30, ["py", "sql"])

x, y = pair
first, *middle, last = (1, 2, 3, 4, 5)
print("empty, single:", empty, single)
print("pair:         ", pair, "->", x, y)
print("named:        ", named, "->", named.x, named.y)
print("first/middle/last:", first, middle, last)

cache = {}
cache[("alice", 2025)] = "report-1"
cache[("alice", 2026)] = "report-2"
print("lookup:", cache[("alice", 2026)])

# Shallow immutability: tuple fixed, inner list still mutable
mixed[2].append("rust")
print("mixed:        ", mixed)

try:
    pair[0] = 99
except TypeError as err:
    print("immutable:    ", err)

Walk through the script noting each idiom:

1) A trailing comma is what makes a one-tuple; `(42)` is just the number in parentheses.
2) Unpacking works positionally; use *rest to pick up the middle of a bigger tuple.
3) Because the outer tuple is hashable, `(name, year)` works as a dict key.
4) The inner list in `mixed` can still be appended to — immutability is shallow.

Try both snippets to internalize unpacking and hashing rules.

# Example A: swap two variables with no temporary
a, b = 1, 2
a, b = b, a
print(a, b)          # 2 1

# Example B: only hashable tuples work as dict keys
valid_key = ("n", 1)
invalid_key = ("n", [1, 2])
seen: dict = {valid_key: True}
try:
    seen[invalid_key] = True
except TypeError as err:
    print("cannot hash:", err)

These checks enforce what the text said.

assert isinstance((1,), tuple) and isinstance((1), int)
assert (1, 2) + (3,) == (1, 2, 3)
assert hash((1, 2)) == hash((1, 2))
a, b, c = (10, 20, 30)
assert (a, b, c) == (10, 20, 30)

Running prints roughly:

empty, single: () (42,)
pair:          (3, 4) -> 3 4
named:         Point(x=3, y=4) -> 3 4
first/middle/last: 1 [2, 3, 4] 5
lookup: report-2
mixed:         ('alice', 30, ['py', 'sql', 'rust'])
immutable:     'tuple' object does not support item assignment