Tuples

📦 Tuples

A tuple is an immutable, ordered sequence — like a list that can never be changed after creation. That constraint turns out to be surprisingly useful: it communicates intent, enables features lists can't have, and is the foundation of Python's elegant multiple-assignment syntax.

⚡ Coming from Java / JavaScript Neither Java nor JavaScript has a built-in tuple type. In Java you'd reach for a small class, a record (Java 16+), or an array. In JavaScript you'd use an array and just... try not to mutate it. Python's tuple is a first-class citizen — lighter than a class, safer than a list, and with unpacking syntax that makes it genuinely powerful.
🚀 FastAPI relevance — MEDIUM Tuples appear frequently in Python web code as lightweight records. When FastAPI reads route parameters or query strings, it often works with named tuples internally. NamedTuple is also a clean, lightweight alternative to full Pydantic models for simple structured data.

🏗️ Creating Tuples

Standard syntax
( item, item, ... )
Parentheses with comma-separated values. The parentheses are actually optional — it's the commas that make a tuple, not the brackets. But use parentheses for clarity.
# With parentheses (recommended) expiry = (2025, 12, 31) ingredient = ("milk", 2, "litres") # Without parentheses — still a tuple! expiry = 2025, 12, 31 print(type(expiry)) # <class 'tuple'>
Single-element tuple
( item, ) ← trailing comma!
This is the single biggest gotcha with tuples. A single value in parentheses is not a tuple — you must add a trailing comma. This catches everyone the first time.
# NOT a tuple — just parentheses not_a_tuple = ("milk") print(type(not_a_tuple)) # <class 'str'> # IS a tuple — trailing comma is_a_tuple = ("milk",) print(type(is_a_tuple)) # <class 'tuple'>
Empty tuple & conversion
() and tuple()
tuple() converts any iterable to a tuple — lists, strings, ranges. The empty tuple is simply ().
empty = () # Convert from list ingredients_list = ["eggs", "milk"] frozen = tuple(ingredients_list) # Convert string to chars chars = tuple("abc") # → ('a', 'b', 'c')
Indexing & slicing
Same as lists
Tuples support all the same indexing and slicing as lists — positive, negative, and start:stop:step. They just can't be modified through those operations.
date = (2025, 8, 15) date[0] # → 2025 (year) date[-1] # → 15 (day) date[1:] # → (8, 15) # This raises TypeError: # date[0] = 2026 ← immutable!

🔒 Immutability — What It Means in Practice

What You Can and Cannot Do
You cannot change, add, or remove items in a tuple. But if a tuple contains a mutable object (like a list), that inner object can still be changed — the tuple just can't point to a different object.
ingredient = ("pasta", 500, "grams") # ✗ All of these raise TypeError # ingredient[0] = "rice" # ingredient.append("organic") # del ingredient[0] # ✓ Reading is fine name = ingredient[0] # "pasta" if "pasta" in ingredient: # membership test print("Found it") # ✓ Concatenation creates a NEW tuple more = ingredient + ("organic",) # → ("pasta", 500, "grams", "organic") # ingredient itself is unchanged # ⚠ The "mutable inside immutable" edge case mixed = (["eggs", "milk"], 500) mixed[0].append("butter") # ✓ modifying the list inside works # mixed[0] is still the same list object — tuple didn't change # mixed[0] = [] ← ✗ this still raises TypeError
Tuples as Dictionary Keys — a Superpower Lists Don't Have
Because tuples are immutable and hashable, they can be used as dictionary keys. Lists cannot. This makes tuples ideal for composite keys — like a (year, month, day) date.
# Use a (year, month, day) tuple as a dict key meal_plan = { (2025, 8, 11): "Spaghetti Bolognese", (2025, 8, 12): "Chicken Tikka", (2025, 8, 13): "Vegetable Curry", } # Look up by date tuple todays_meal = meal_plan[(2025, 8, 12)] print(todays_meal) # → "Chicken Tikka" # This would raise TypeError — lists are not hashable: # bad = {[2025, 8, 11]: "Pasta"} ← ✗

📬 Tuple Unpacking — Python's Party Trick

Unpacking is arguably the most useful thing about tuples. It lets you assign multiple variables in one clean line and is used constantly in real Python code.

Basic Unpacking
The number of variables on the left must match the number of values in the tuple (unless you use *). The variable names can be anything — Python assigns by position.
# Unpack into named variables expiry = (2025, 12, 31) year, month, day = expiry print(f"Expires: {day}/{month}/{year}") # 31/12/2025 # Unpack an ingredient record ingredient = ("milk", 2, "litres") name, qty, unit = ingredient print(f"{qty} {unit} of {name}") # 2 litres of milk # Swap two variables — no temp variable (works because of tuple packing) a, b = 10, 20 a, b = b, a print(a, b) # 20 10
Starred Unpacking — Grab the Rest
Use * to capture multiple remaining values into a list. The starred variable can be in any position.
ingredients = ("flour", "eggs", "milk", "butter", "sugar") # Grab first, last, and everything in between first, *middle, last = ingredients print(first) # "flour" print(middle) # ["eggs", "milk", "butter"] ← a list, not a tuple print(last) # "sugar" # Grab just the first, ignore the rest head, *_ = ingredients print(head) # "flour" # Grab just the last two *_, penultimate, final = ingredients print(penultimate, final) # "butter" "sugar"
Ignore Values with _ (Underscore)
By convention, _ is used for values you don't need. It's a real variable, but it signals to other developers (and yourself) that you intentionally don't care about that value.
date_record = (2025, 8, 15, "Thursday") # Only want the month and day _, month, day, _ = date_record print(f"{day}/{month}") # 15/8 # Unpack inside a for loop — very common pattern inventory = [ ("milk", 2, 3), # (name, qty, days_left) ("eggs", 6, 10), ("yogurt", 1, 1), ] for name, qty, days_left in inventory: if days_left <= 3: print(f"⚠ {name} expires in {days_left} day(s)!")

🏷️ Named Tuples — Tuples with Labels

A named tuple is a tuple subclass where each position has a name. It gives you the immutability and memory efficiency of a tuple with the readability of a class — without needing to write a full class definition.

typing.NamedTuple — The Modern Approach
There are two ways to create named tuples. The modern way using typing.NamedTuple supports type hints and is more readable — it's what you'll see in FastAPI-adjacent code.
from typing import NamedTuple # Define a named tuple (looks like a class) class Ingredient(NamedTuple): name: str quantity: int unit: str days_left: int = 0 # default value # Create instances milk = Ingredient("milk", 2, "litres", 3) eggs = Ingredient("eggs", 6, "units", 10) butter = Ingredient(name="butter", quantity=250, unit="grams", days_left=7) # Access by NAME — much clearer than index print(milk.name) # "milk" print(milk.days_left) # 3 # Still works as a tuple too print(milk[0]) # "milk" name, qty, unit, days = milk # unpacking still works # Expiry alert using named access pantry = [milk, eggs, butter] for item in pantry: if item.days_left <= 3: print(f"⚠ {item.name}: only {item.days_left} day(s) left!")
collections.namedtuple — The Classic Approach
The older collections.namedtuple factory function does the same thing with a different syntax. You'll see this in older codebases.
from collections import namedtuple # First arg: the type name. Second: field names as a list (or space-separated string) Recipe = namedtuple("Recipe", ["name", "servings", "prep_minutes"]) pasta = Recipe("Pasta", 4, 30) print(pasta.name) # "Pasta" print(pasta.servings) # 4 # Convert to dict print(pasta._asdict()) # → {"name": "Pasta", "servings": 4, "prep_minutes": 30} # "Update" a field — returns a NEW named tuple (immutable!) bigger = pasta._replace(servings=8) print(bigger.servings) # 8 print(pasta.servings) # 4 — original unchanged

⚖️ Tuple vs List — When to Use Which

📦 Use a Tuple when...
  • The data is fixed and shouldn't change
  • You're grouping related but different things (name, qty, unit)
  • You need to use it as a dictionary key
  • You want to signal to other developers: "don't modify this"
  • You need slight memory and speed advantages over a list
  • Returning multiple values (covered in the functions lesson)
📋 Use a List when...
  • The data will grow or shrink over time
  • You're collecting similar things (a list of ingredients)
  • You need to sort, append, remove, or reorder
  • The length isn't known in advance
  • You're building up results in a loop
  • You want list comprehensions (comprehensions return lists)
A Practical Rule of Thumb
Think of a list as a collection of the same kind of thing. Think of a tuple as a record with fixed fields.
# List — many things of the same kind shopping = ["milk", "eggs", "bread"] # will be added to / removed from # Tuple — one thing with multiple attributes milk = ("milk", 2, "litres", 3) # fixed record: name, qty, unit, days # List of tuples — very common pattern inventory = [ ("milk", 2, "litres", 3), ("eggs", 6, "units", 10), ("butter", 250, "grams", 7), ] # Sort by days_left (index 3) — tuples sort by first element by default inventory.sort(key=lambda item: item[3]) # → milk (3), butter (7), eggs (10)

📋 Quick Reference

OperationSyntaxNotes
Create(1, 2, 3)Parentheses optional — commas make the tuple
Single item(1,)Trailing comma is required — (1) is just 1
Empty()Or tuple()
From iterabletuple([1, 2, 3])Converts list, string, range, etc.
Indext[0] / t[-1]Same as lists — read only
Slicet[1:3]Returns a new tuple
Lengthlen(t)
Membershipx in tReturns True / False
Concatenatet1 + t2Creates a new tuple
Repeatt * 3e.g. (0,) * 7 → seven zeros
Countt.count(x)How many times x appears
Findt.index(x)Index of first x — raises ValueError if missing
Unpacka, b, c = tMust match length exactly
Starred unpackfirst, *rest = trest becomes a list
Ignore value_, b, _ = tConvention for "don't care"
As dict key{(1, 2): "val"}Works because tuples are hashable
Named tupleclass T(NamedTuple): ...Access by name: t.name
To listlist(t)When you need to modify it
⚠ Gotchas for Java / JavaScript Programmers:

The single-element trap("milk") is a string, not a tuple. ("milk",) is a tuple. The trailing comma is the only difference and it will bite you at least once.

Tuples are not "read-only lists" — they are a different type with a different purpose. A tuple says "this is a record with a fixed structure." A list says "this is a collection of similar things." Choosing the right one communicates intent to anyone reading your code.

Immutability is shallow — if a tuple contains a list, that list can still be mutated. The tuple's references are frozen, not the objects those references point to. This is the same as Java's final for object references — final List<String> list prevents reassignment but not list.add().

Starred unpacking gives a list, not a tuplefirst, *rest = (1, 2, 3, 4) gives rest = [2, 3, 4] as a list, even though the source was a tuple. If you need a tuple, wrap it: rest = tuple(rest).