Tuples

Tuples are immutable, ordered sequences — Python's go-to for fixed-size records and for grouping values that shouldn't change.

A tuple is an ordered, immutable sequence, written with parentheses (or often no brackets at all, since commas do the real work). Once created, a tuple's set of element references can never change: no append, no item assignment, no removal. That immutability makes tuples hashable (when their contents are), safe to share, and a natural fit for fixed structural data like coordinates or database rows.

Creating Tuples

Tuples are created with parentheses, the tuple() constructor, or simply by writing comma-separated values — the comma, not the parentheses, is what actually makes a tuple.

point = (3, 4)
also_a_tuple = 3, 4          # parentheses are optional here
empty: tuple[int, ...] = ()
single_value = tuple([1, 2, 3])   # (1, 2, 3), built from a list
one_item = (5,)               # trailing comma required -- see below

The Single-Element Tuple Trap

Parentheses around one value do not make a tuple by themselves — Python treats (5) as the plain integer 5. A trailing comma is what signals a tuple, with or without parentheses.

not_a_tuple = (5)
type(not_a_tuple)     # <class 'int'>

is_a_tuple = (5,)
type(is_a_tuple)      # <class 'tuple'>

also_a_tuple = 5,     # same thing, parentheses omitted
type(also_a_tuple)    # <class 'tuple'>

Indexing and Slicing

Tuples support the same read-only sequence operations as lists: indexing (including negative indices) and slicing. A slice of a tuple returns a new tuple.

t = (10, 20, 30, 40)
t[0]      # 10
t[-1]     # 40
t[1:3]    # (20, 30)
t[::-1]   # (40, 30, 20, 10)

Immutability

A tuple cannot be resized or have its elements reassigned; attempting item assignment raises TypeError. Immutability applies to the tuple's own set of references, not necessarily to the objects it references — a tuple can hold a mutable object (like a list), and that object's contents can still change in place.

Because a tuple can hold mutable elements, a tuple is not always hashable -- see the hashability section below.

t = (1, 2, 3)
t[0] = 99            # TypeError: 'tuple' object does not support item assignment

nested = ([1, 2], "fixed")
nested[0].append(3)  # OK -- mutating the LIST inside the tuple
print(nested)         # ([1, 2, 3], 'fixed')
nested[0] = [9]        # still TypeError -- can't rebind the tuple's slots

Packing and Unpacking

Tuple packing groups multiple values into one tuple; unpacking spreads a tuple's values back into separate names. The starred (*) target collects a variable number of remaining items into a list during unpacking.

# packing
point = 3, 4, 5   # a 3-tuple

# unpacking
x, y, z = point
print(x, y, z)    # 3 4 5

# swap idiom -- built on packing/unpacking
a, b = 1, 2
a, b = b, a       # a=2, b=1

# starred unpacking
first, *middle, last = [1, 2, 3, 4, 5]
print(first, middle, last)  # 1 [2, 3, 4] 5

Tuples as Return Values and Multiple Assignment

A function that appears to 'return multiple values' is really returning one tuple, which the caller then unpacks. This is the idiomatic way to hand back several related results.

def divmod_custom(a: int, b: int) -> tuple[int, int]:
    return a // b, a % b     # packs a tuple

quotient, remainder = divmod_custom(17, 5)
print(quotient, remainder)   # 3 2

Hashability Caveats

A tuple is hashable, and therefore usable as a dict key or set element, only if every one of its elements is hashable. A tuple containing a list (or any other mutable, unhashable object) is itself unhashable.

ok = (1, "a", (2, 3))
hash(ok)                 # works -- all elements are hashable

bad = (1, [2, 3])
hash(bad)                # TypeError: unhashable type: 'list'

coords: dict[tuple[int, int], str] = {(0, 0): "origin", (1, 1): "diag"}

Tuple Methods and Membership

Because tuples are immutable, they support only two methods: count() and index(). Membership testing works the same as for lists (linear scan, O(n)).

t = (1, 2, 2, 3)
t.count(2)     # 2
t.index(3)     # 3
3 in t          # True

Named Tuples: A Related Concept

When fields deserve names instead of positions, the standard library's collections.namedtuple (or typing.NamedTuple for a typed, class-based form) builds tuple subclasses with named attributes while keeping full tuple compatibility. This is a related concept, not covered in depth here.

from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int

p = Point(3, 4)
p.x, p[0]          # 3 3 -- named AND positional access both work

Practical Usage and Tuples vs Lists

Reach for a tuple when the collection represents a fixed-size, heterogeneous record (a coordinate, an RGB color, a row returned from a database query) or when you need a hashable, immutable key. Reach for a list when the collection is homogeneous and its size or contents will change over its lifetime.

rgb = (255, 128, 0)             # fixed structure -- tuple fits
scores = [88, 92, 79]            # will grow/shrink -- list fits
visited: set[tuple[int, int]] = {(0, 0), (0, 1)}  # tuple as hashable key

Key Takeaways

Tuples are ordered, immutable sequences; the comma creates the tuple, not the parentheses.

A single-element tuple needs a trailing comma: (5,), not (5).

Immutability applies to the tuple's own slots, not necessarily to mutable objects stored inside it.

Packing/unpacking (including starred unpacking) is the idiomatic way to build and consume tuples.

A tuple is hashable only if all of its elements are hashable, which is why it can be a dict key but a list never can.

Only count() and index() are defined as tuple methods, reflecting their immutability.

single = (5) # this is just int 5 single = (5,) # trailing comma makes it a tuple

Parentheses alone don't create a tuple; Python needs the comma to distinguish a tuple literal from a parenthesized expression.

t[0] = 99 # TypeError t = (99,) + t[1:] # build a new tuple instead

Tuples cannot be mutated in place; 'changing' one means constructing a new tuple from the old one's pieces.

cache = {} cache[([1, 2])] = "x" # TypeError: unhashable cache[(1, 2)] = "x" # plain tuple of hashable ints

A tuple containing a mutable element like a list is itself unhashable and cannot be used as a dict key.

Why is (5) not a tuple but (5,) is?

Parentheses in Python are also used for grouping expressions, so (5) is just the integer 5 in parentheses; the comma is the actual tuple constructor, so a trailing comma is required for a one-element tuple.

Can a tuple's contents ever appear to change? Explain with an example.

Yes if it holds a mutable element: the tuple's own references can't be reassigned, but a list stored inside it can still be mutated in place, e.g. t = ([1], 2); t[0].append(9) changes t's printed value without violating tuple immutability.

What determines whether a tuple can be used as a dictionary key?

All of its elements must be hashable; a tuple of hashable, immutable elements is itself hashable, but a tuple containing a list, dict, or set is not.

What does a, b = b, a do and why does it work without a temporary variable?

It packs the right-hand side into a temporary tuple (b, a) first, then unpacks that tuple into a and b, so both assignments effectively happen simultaneously from the original values.

What is the result of first, *rest = (1, 2, 3, 4) and what type is rest?

first is 1 and rest is the list [2, 3, 4]; starred unpacking always produces a list for the collected remainder, even though the source was a tuple.

When would you choose a tuple over a list for a function's return value?

When returning a fixed number of related, heterogeneous values (like a status code and a message) that the caller will unpack positionally and that shouldn't be mutated afterward.

Write a function that takes a list of (name, score) tuples and returns the name with the highest score.

def top_scorer(records: list[tuple[str, int]]) -> str: return max(records, key=lambda r: r[1])[0] print(top_scorer([("Ana", 88), ("Bo", 95), ("Cy", 91)])) # Bo max() with key=lambda r: r[1] finds the tuple with the largest score, and [0] extracts that record's name, giving 'Bo'.

What prints, and what happens on the final line?

It prints (1, [2, 3, 5], 4) -- mutating the inner list is allowed. The final line t[1] = [9] raises TypeError because item assignment on the tuple itself is not allowed, regardless of what that slot holds.

Write a function returning both the minimum and maximum of a list as a single tuple, in one pass.

def min_max(nums: list[int]) -> tuple[int, int]: if not nums: raise ValueError("nums must not be empty") lo = hi = nums[0] for n in nums[1:]: if n < lo: lo = n elif n > hi: hi = n return lo, hi print(min_max([4, 1, 7, 2, 9, 0])) # (0, 9)

The function tracks lo and hi while scanning once, then packs them into a tuple on return; the caller can unpack it as low, high = min_max(data).

t = (1, [2, 3], 4)
t[1].append(5)
print(t)
t[1] = [9]