learn.aathan.in

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.

345 (3, 4, 5) xyz pack unpack
Packing collects loose values into a tuple; unpacking assigns them straight into variables — the count on each side must match (unless you use *).
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?

TupleList
Mutable?No (fixed)Yes
Can be a dict key / set member?Yes (if items hashable)No
Methods2 (count, index)many
Memory / speedslightly leaner & fasterflexible
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 just 5. 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 in tuple() 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])) raises TypeError.

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 namedtuple when a fixed record deserves field names.