Python – Tuples

July 16, 20264 min readUpdated 8/20/2026

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.