A tuple is a sequence you cannot change. That sounds like a list with a feature removed, which is why it gets skipped — but immutability is the point, and tuples turn up constantly whether you write them or not.
Making one
point = (3, 4)
account = ("Checking", "1001-0001", 1250.00)
print(account[0]) # Output: Checking
print(account[-1]) # Output: 1250.0
print(len(account)) # Output: 3
print(account[:2]) # Output: ('Checking', '1001-0001')
Everything you can do to read a list works here — indexing, negative indexes, slicing,
len(), in, looping. What is missing is everything that would change it: no
append, no sort, no assignment to an index.
It is the commas that make a tuple, not the parentheses. The brackets are usually optional and are there for clarity:
account = "Checking", 1250.00
print(type(account).__name__) # Output: tuple
The one-element trap
Because the comma does the work, a single-element tuple needs a trailing one:
not_a_tuple = ("Checking")
real_tuple = ("Checking",)
print(type(not_a_tuple).__name__) # Output: str
print(type(real_tuple).__name__) # Output: tuple
print(len(real_tuple)) # Output: 1
The first is just a string in brackets. This catches everybody once, usually when passing a single argument to something that expects a tuple, and the symptom is a function iterating over the characters of your string.
Unpacking
This is where tuples earn their place. Assign a tuple to several names at once and Python distributes the pieces:
account = ("Checking", "1001-0001", 1250.00)
kind, number, balance = account
print(f"{kind} {number} holds {balance:,.2f}")
# Output: Checking 1001-0001 holds 1,250.00
a, b = 1, 2
a, b = b, a
print(a, b) # Output: 2 1
The swap on the last line needs no temporary variable — the right-hand side is built as a tuple first, then unpacked. It is the standard Python idiom for a swap.
Unpacking is why you have been writing tuples without noticing.
for name, balance in balances.items() and
for i, item in enumerate(items) both hand you a tuple per turn and unpack it in the
for statement.
Returning more than one value
def apply_fee(balance, fee):
"""Return the new balance and whether the fee could be taken."""
if balance < fee:
return balance, False
return balance - fee, True
new_balance, charged = apply_fee(100.0, 15.0)
print(new_balance, charged) # Output: 85.0 True
new_balance, charged = apply_fee(10.0, 15.0)
print(new_balance, charged) # Output: 10.0 False
Python has no out-parameters and needs none: return a tuple and unpack it at the call site. There is
no wrapper class and no ceremony. Beyond three values it becomes hard to remember the order, and that
is the point to return a dataclass or a
NamedTuple instead.
The starred target
line = "1,alice@bank.test,Alice,Cooper,2020-03-06"
user_id, email, *name_parts, created = line.split(",")
print(user_id) # Output: 1
print(name_parts) # Output: ['Alice', 'Cooper']
print(created) # Output: 2020-03-06
One name may be starred, and it absorbs however many items are left over — always as a list, even when it catches one item or none. Useful for "the first, the last, and whatever is in between" without counting.
Named tuples, when positions stop being obvious
account[2] is fine when you wrote the line and unreadable a month later.
NamedTuple keeps the tuple and adds names:
from typing import NamedTuple
class Account(NamedTuple):
kind: str
number: str
balance: float
checking = Account("Checking", "1001-0001", 1250.00)
print(checking.balance) # Output: 1250.0
print(checking[2]) # Output: 1250.0
print(checking.kind) # Output: Checking
kind, number, balance = checking
print(number) # Output: 1001-0001
It is still a real tuple — indexable, unpackable, immutable — with attribute access on top. For a small immutable record it costs four lines and repays them every time someone reads the code. When you want something mutable or with methods, use a dataclass.
Comparing and sorting
Tuples compare element by element, left to right, stopping at the first difference. That gives you multi-key sorting for free:
print((1, "b") < (1, "c")) # Output: True
print((2, "a") < (1, "z")) # Output: False
accounts = [
("Savings", 8400.50),
("Checking", 1250.00),
("Checking", 90.00),
]
for kind, balance in sorted(accounts):
print(f"{kind} {balance:,.2f}")
# Output: Checking 90.00
# Output: Checking 1,250.00
# Output: Savings 8,400.50
Sorted by kind, then by balance within each kind, with no key function at all. When the
natural order is not the one you want, return a tuple from the key —
key=lambda a: (a.kind, -a.balance) sorts by kind ascending and balance descending, which
is the standard trick for a two-column sort.
Where immutability actually matters
A tuple can be a dictionary key or a set member; a list cannot.
visits = {}
visits[("2026-08-20", "checking")] = 4
visits[("2026-08-20", "savings")] = 1
print(visits[("2026-08-20", "checking")]) # Output: 4
That is a compound key, and it is the everyday reason to reach for a tuple deliberately. It works
because a tuple's contents cannot change, so its hash cannot change either — the guarantee a
dictionary depends on. Try it with a list and you get
TypeError: unhashable type: 'list'.
Which to use
The useful test is not "will this change" but "is this one thing, or many of the same thing?"
- Tuple — a fixed number of parts making up one record. The position means
something:
(kind, number, balance). - List — any number of interchangeable items. The position means nothing:
["Checking", "Savings", "Credit"].
By that test, three fields of an account is a tuple even though it never changes, and a list of accounts is a list even if you never append to it. Tuples are also slightly smaller and faster, which is true and almost never the reason to choose one.
Next: Dictionaries & Sets.