Tuples are ordered, immutable sequences — once created, their contents cannot be changed. This immutability is not a limitation but a feature: it signals to readers and tools that this collection represents a fixed record whose values should not change. Python uses tuples internally everywhere — function multiple return values, enumerate() and zip() produce tuples, and SQLAlchemy row proxies behave like tuples. Understanding when to use a tuple instead of a list — and mastering tuple unpacking — will make your FastAPI and database code more readable and intentional.
Creating and Accessing Tuples
# Create with parentheses (or just commas)
point = (10, 20)
rgb = (255, 128, 0)
single = (42,) # trailing comma required for single-element tuple
also_tuple = 42, # parentheses optional — comma makes it a tuple
empty = ()
# No parentheses — the comma creates the tuple
x, y, z = 1, 2, 3 # multiple assignment is tuple packing/unpacking
# Indexing and slicing — same as lists
point[0] # 10
point[-1] # 20
rgb[0:2] # (255, 128)
# Length and membership
len(rgb) # 3
255 in rgb # True
# Tuples are immutable — these all raise TypeError:
# point[0] = 5
# point.append(30)
# del point[0]
# But if a tuple contains a mutable object, that object can change:
mixed = ([1, 2], "hello")
mixed[0].append(3) # OK! The list inside is mutable
print(mixed) # ([1, 2, 3], "hello")
(42,). Without it, (42) is just the integer 42 in parentheses — Python uses parentheses for grouping expressions, not for creating tuples. The comma is what makes a tuple, not the parentheses. This is a common Python gotcha: type((42)) is int, but type((42,)) is tuple.(x, y), a colour (r, g, b), a database row (id, name, email). Use a list when the collection is a sequence that will grow, shrink, or be reordered. The choice communicates intent: a tuple says “these values belong together and form a complete record”, a list says “this is a variable-length sequence of similar items”.cache[(user_id, post_id)] = result.Tuple Unpacking
# Unpack a tuple into named variables
point = (10, 20)
x, y = point # x=10, y=20
rgb = (255, 128, 0)
red, green, blue = rgb
# Swap variables using tuple packing/unpacking (no temp variable!)
a, b = 1, 2
a, b = b, a # a=2, b=1
# Extended unpacking with * (star)
first, *rest = [1, 2, 3, 4, 5]
print(first) # 1
print(rest) # [2, 3, 4, 5]
*most, last = [1, 2, 3, 4, 5]
print(most) # [1, 2, 3, 4]
print(last) # 5
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
# Ignore values with _ (convention for unused variables)
_, y = (10, 20) # only care about y
x, _, z = (1, 2, 3) # ignore the middle value
# Nested unpacking
data = ((1, 2), (3, 4))
(a, b), (c, d) = data # a=1, b=2, c=3, d=4
# Unpack in a for loop
pairs = [(1, "one"), (2, "two"), (3, "three")]
for number, word in pairs:
print(f"{number} = {word}")
Returning Multiple Values from Functions
# Python "returns multiple values" by packing into a tuple
def get_user_stats(user_id: int):
# ... database query ...
return 42, 150, 3.8 # packs into a tuple (42, 150, 3.8)
# Unpack the returned tuple
post_count, view_total, avg_rating = get_user_stats(1)
# Or keep as a tuple
stats = get_user_stats(1)
print(stats[0]) # 42 — post_count
# FastAPI pattern: return metadata alongside data
def paginate_query(query, page: int, limit: int):
total = query.count()
items = query.offset((page - 1) * limit).limit(limit).all()
return items, total, page # tuple of (data, metadata)
posts, total, current_page = paginate_query(db.query(Post), 1, 10)
Named Tuples
from collections import namedtuple
# Create a named tuple class — fields have names, not just positions
Point = namedtuple("Point", ["x", "y"])
Colour = namedtuple("Colour", ["red", "green", "blue"])
User = namedtuple("User", ["id", "name", "email", "role"])
# Create instances
origin = Point(0, 0)
red = Colour(255, 0, 0)
alice = User(1, "Alice", "alice@example.com", "admin")
# Access by name (readable) OR by index (tuple-compatible)
print(alice.name) # "Alice" — by name
print(alice[1]) # "Alice" — by index
print(alice.email) # "alice@example.com"
# Still immutable — alice.name = "Bob" raises AttributeError
# Unpack like a regular tuple
user_id, name, email, role = alice
# Convert to dict
alice._asdict() # {"id": 1, "name": "Alice", ...}
# Replace a field (returns a new namedtuple)
bob = alice._replace(name="Bob", email="bob@example.com")
# Useful for database rows before Pydantic models exist
Row = namedtuple("Row", ["id", "title", "published"])
db_row = Row(1, "My Post", True)
print(db_row.title) # "My Post"
Common Mistakes
Mistake 1 — Single-element tuple missing the comma
❌ Wrong — not a tuple, just a parenthesised integer:
t = (42)
type(t) # <class 'int'> — not a tuple!
✅ Correct — trailing comma creates the tuple:
t = (42,)
type(t) # <class 'tuple'> ✓
Mistake 2 — Trying to modify an immutable tuple
❌ Wrong — attempting to assign to a tuple index:
point = (1, 2)
point[0] = 10 # TypeError: 'tuple' object does not support item assignment
✅ Correct — create a new tuple with the modified value:
point = (10, point[1]) # new tuple with updated x ✓
Mistake 3 — Wrong number of variables in unpacking
❌ Wrong — mismatch between tuple length and variable count:
a, b = (1, 2, 3) # ValueError: too many values to unpack
✅ Correct — match the count or use * to capture extras:
a, b, c = (1, 2, 3) # exact match ✓
a, *rest = (1, 2, 3) # a=1, rest=[2, 3] ✓
Quick Reference
| Operation | Code |
|---|---|
| Create tuple | (1, 2, 3) or 1, 2, 3 |
| Single element | (42,) — trailing comma required |
| Unpack | a, b = (1, 2) |
| Star unpack | first, *rest = items |
| Ignore value | _, y = point |
| Swap | a, b = b, a |
| Named tuple class | Point = namedtuple("Point", ["x", "y"]) |
| Named tuple to dict | p._asdict() |
| Replace field | p._replace(x=5) |