Python Tuples: The Complete Guide
Everything about Python tuples — creating (and the one-element comma trap), packing & unpacking, immutability, slicing, the two methods, named tuples, when to use a tuple vs a list — with a runnable example for each.
A tuple is an ordered, immutable sequence. Think of it as a
list that can’t be changed after you make it. That
one restriction — immutability — is what makes tuples useful: they’re safe to
share, they can be dict keys and set members, and they’re the natural
way to bundle a few related values (“a record”) together. This guide covers
every basic concept, with a short runnable example for each. Outputs are shown
as # => ... comments.
What makes a tuple a tuple
- Ordered — items keep their position; index and slice like a list.
- Immutable — you can’t add, remove, or reassign items after creation.
- Heterogeneous — any mix of types.
- Hashable (if its items are) — so it can be a dict key or set element.
point = (3, 4)
point[0] # => 3
point[0] = 9 # TypeError: 'tuple' object does not support item assignment
Creating tuples
The comma is what makes a tuple — not the parentheses. The parens are usually there just for clarity.
t = (1, 2, 3) # literal
t = 1, 2, 3 # same thing! parentheses optional ("tuple packing")
empty = () # empty tuple
empty = tuple() # also empty
from_list = tuple([1, 2, 3]) # => (1, 2, 3) (from any iterable)
from_str = tuple("abc") # => ('a', 'b', 'c')
nested = (1, (2, 3), [4, 5]) # tuples can hold anything
The one-element trap ⚠️
A single value in parentheses is not a tuple — the trailing comma is what makes it one. This bites everyone once.
not_a_tuple = (5) # => 5 (just the number 5 in parens)
type(not_a_tuple) # => <class 'int'>
a_tuple = (5,) # => (5,) ← the comma makes it a tuple
type(a_tuple) # => <class 'tuple'>
also = 5, # => (5,) (parens optional here too)
Indexing & slicing
Exactly like lists — but slicing returns a new tuple.
t = ('p', 'y', 't', 'h', 'o', 'n')
t[0] # => 'p'
t[-1] # => 'n' (negative index from the end)
t[1:4] # => ('y', 't', 'h') (slice → new tuple)
t[:3] # => ('p', 'y', 't')
t[::-1] # => ('n', 'o', 'h', 't', 'y', 'p') (reversed)
t[::2] # => ('p', 't', 'o') (every 2nd, via step)
Immutable — but read this carefully
You can’t change which objects a tuple holds. But if one of those objects is itself mutable (like a list), that object can still change.
t = (1, 2, 3)
t.append(4) # AttributeError: tuples have no append
t[0] = 99 # TypeError: item assignment not supported
# The tuple is fixed, but a mutable item inside it isn't:
t = (1, [2, 3])
t[1].append(4) # OK! → t is now (1, [2, 3, 4])
t[1] = [9] # still an error — you can't reassign the slot
And += doesn’t mutate a tuple — it rebinds the name to a brand-new tuple:
t = (1, 2)
old_id = id(t)
t += (3,) # t => (1, 2, 3), but it's a NEW object
id(t) == old_id # => False
The workaround: change a tuple via a list
Tuples are unchangeable — but you can convert to a list, change it, and convert back. This is the standard trick when you need “just one edit”.
# CHANGE an item
x = ("apple", "banana", "cherry")
y = list(x) # tuple -> list
y[1] = "kiwi" # lists are mutable, so edit freely
x = tuple(y) # list -> tuple
print(x) # => ('apple', 'kiwi', 'cherry')
# ADD an item
thistuple = ("apple", "banana", "cherry")
y = list(thistuple)
y.append("orange")
thistuple = tuple(y)
print(thistuple) # => ('apple', 'banana', 'cherry', 'orange')
# REMOVE an item
thistuple = ("apple", "banana", "cherry")
y = list(thistuple)
y.remove("apple")
thistuple = tuple(y)
print(thistuple) # => ('banana', 'cherry')
For adding specifically, there’s a shorter route — concatenate another tuple (remember the trailing comma for a single item):
thistuple = ("apple", "banana", "cherry")
y = ("orange",) # a one-item tuple
thistuple += y
print(thistuple) # => ('apple', 'banana', 'cherry', 'orange')
Deleting the whole tuple
You can’t delete items, but del removes the entire variable:
thistuple = ("apple", "banana", "cherry")
del thistuple
print(thistuple) # NameError: name 'thistuple' is not defined
The only two methods
Because tuples can’t be modified, they have just two methods (both read-only):
t = (1, 2, 2, 3, 2)
t.count(2) # => 3 (how many 2s)
t.index(2) # => 1 (first position of 2; ValueError if missing)
t.index(2, 2) # => 2 (search starting at index 2)
# Full syntax: tuple.index(value, start, end) - search only within a range
nums = (1, 3, 7, 8, 7, 5, 4, 6, 8, 5)
nums.index(8, 5) # => 8 search from index 5 onward
nums.index(7, 4, 7) # => 4 search only within indices 4..6
Everything else you do to a tuple is a built-in, not a method:
t = (3, 1, 4, 1, 5)
len(t) # => 5
min(t) # => 1
max(t) # => 5
sum(t) # => 14
3 in t # => True (membership)
sorted(t) # => [1, 1, 3, 4, 5] ← returns a LIST, not a tuple
tuple(sorted(t)) # => (1, 1, 3, 4, 5) (convert back if you want a tuple)
Concatenation & repetition (make new tuples)
(1, 2) + (3, 4) # => (1, 2, 3, 4) (join → new tuple)
(0,) * 3 # => (0, 0, 0) (repeat)
Packing & unpacking — the superpower
This is what tuples are for. Packing bundles values; unpacking spreads them back into variables.
*).packed = 3, 4, 5 # packing → (3, 4, 5)
x, y, z = packed # unpacking → x=3, y=4, z=5
x, y, z = (3, 4, 5) # parens optional on either side
# The famous one-line swap (no temp variable):
a, b = 1, 2
a, b = b, a # a=2, b=1
# Star (*) grabs "the rest" as a LIST:
first, *rest = (1, 2, 3, 4) # first=1, rest=[2, 3, 4]
*init, last = (1, 2, 3, 4) # init=[1, 2, 3], last=4
a, *mid, b = (1, 2, 3, 4, 5) # a=1, mid=[2, 3, 4], b=5
# Nested unpacking mirrors the structure:
(a, b), c = (1, 2), 3 # a=1, b=2, c=3
# Ignore values you don't want with _ :
_, y, _ = (1, 2, 3) # keep only the middle → y=2
Unpacking shows up everywhere — in for loops, over enumerate, and zip:
pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
for num, letter in pairs: # unpack each tuple per iteration
print(num, letter) # 1 a / 2 b / 3 c
for i, val in enumerate(['x', 'y']): # enumerate yields (index, value) tuples
print(i, val) # 0 x / 1 y
Returning multiple values
Functions “return multiple values” by returning one tuple — which the caller unpacks. This is the most common reason to use a tuple.
def min_max(nums):
return min(nums), max(nums) # packs into a tuple
lo, hi = min_max([4, 1, 7, 3]) # unpacks → lo=1, hi=7
result = min_max([4, 1, 7, 3]) # or keep the tuple → (1, 7)
Tuples as dict keys & set members
Because a tuple of hashable items is itself hashable, it can go where a list never could — as a dict key or in a set. Perfect for composite keys like coordinates.
board = {}
board[(0, 0)] = 'X' # a coordinate as a key
board[(1, 2)] = 'O'
board[(0, 0)] # => 'X'
seen = set()
seen.add((3, 4)) # ✓ tuple in a set
seen.add([3, 4]) # TypeError: unhashable type: 'list'
Comparing tuples
Tuples compare element by element, left to right (lexicographic) — like words in a dictionary. This makes them great for multi-key sorting.
(1, 2, 3) == (1, 2, 3) # => True
(1, 2) < (1, 3) # => True (first differ at index 1: 2 < 3)
(1, 2) < (1, 2, 0) # => True (a prefix is "less than")
# Sort records by multiple fields at once — sort() compares the tuples:
people = [("Bo", 30), ("Al", 30), ("Cy", 25)]
sorted(people, key=lambda p: (p[1], p[0]))
# => [('Cy', 25), ('Al', 30), ('Bo', 30)] (by age, then name)
Named tuples — tuples with field names
collections.namedtuple gives you a tuple whose fields also have names, so
you write p.x instead of p[0] — readable and still a real tuple.
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
p.x # => 3 (access by name)
p[0] # => 3 (still index-able like a tuple)
x, y = p # => still unpacks
p.x = 9 # AttributeError — still immutable
# Handy extras:
p._asdict() # => {'x': 3, 'y': 4}
p._replace(x=9) # => Point(x=9, y=4) (returns a NEW one)
Tuples vs lists — which to use?
| Tuple | List | |
|---|---|---|
| Mutable? | No (fixed) | Yes |
| Can be a dict key / set member? | Yes (if items hashable) | No |
| Methods | 2 (count, index) | many |
| Memory / speed | slightly leaner & faster | flexible |
| Signals intent | ”a fixed record of related values" | "a collection that will change” |
Rule of thumb:
- Tuple for a fixed group of things where position has meaning — a
coordinate
(x, y), an RGB colour(255, 0, 0), a database row, a function returning several values, a dict key. - List for a homogeneous collection you’ll add to, remove from, or reorder.
Gotchas recap
(5)is not a tuple — it’s just5. You need the comma:(5,).- Immutable ≠ deeply immutable — a tuple can contain a mutable list whose contents can still change.
sorted(t)returns a list, not a tuple — wrap intuple()if needed.t += (x,)makes a new tuple; the original object is unchanged.- A tuple with a mutable item inside is not hashable, so it can’t be a dict
key:
hash((1, [2]))raisesTypeError.
Takeaways
- A tuple is an ordered, immutable sequence — a list you promise not to change. The comma makes it, not the parentheses.
- Its real jobs: packing/unpacking (multiple assignment, swaps, multiple return values, loop targets), and being a hashable key for dicts/sets.
- Only two methods (
count,index); everything else is a built-in. - Reach for
namedtuplewhen a fixed record deserves field names.