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 belowThe 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 slotsPacking 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] 5Tuples 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 2Hashability 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 # TrueNamed 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 workPractical 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 keyKey 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]