Tuples and Unpacking

A sequence that cannot be changed after it is built, and the syntax that takes one apart in a single line.

Overview

The difference that matters

A list and a tuple both hold an ordered run of items, and both index the same way. The difference is one word: a tuple is immutable. Once it exists, no element can be replaced.

point = (3, 4)
point[0] = 99  # TypeError

That sounds like a limitation and is mostly a guarantee. If you hand a list to a function, the function can change it under you. Hand it a tuple and it cannot.

tuples.py

tuples.py Python 3
Output

                    

why_tuples.py

why_tuples.py Python 3
Output

                    

Worth knowing

A tuple is written with commas, not brackets. 1, 2 is already a tuple; the brackets only group it.
Immutable means the tuple cannot be re-pointed, so it can be a dict key or a set member. A list cannot.
a, b = b, a builds a tuple on the right, then unpacks it. That is why the swap needs no temporary.
One element needs a trailing comma: (5,). Without it you have brackets round a number.

Tuples and Unpacking: A Practical Guide

A tuple is a sequence that cannot be changed once built. That single restriction is what makes it usable as a dictionary key, safe to hand around, and the natural way to return more than one value.

Why immutability buys you a dictionary key

A dictionary key has to be hashable, which in practice means it must never change — if it changed, the dictionary would look for it in the wrong place. Lists are mutable, so they are unhashable, so they cannot be keys. Tuples can.

grid[(2, 3)] = "wall"

That is how you key anything by a coordinate pair, a date triple, or any other small fixed group.

Unpacking is the point

Unpacking assigns each position to a name in one statement:

x, y = point

It is not a special tuple feature — it works on any sequence — but tuples are where you meet it. The number of names must match the number of items, or Python raises ValueError, which is a feature: it catches the case where the shape you expected is not the shape you got.

The swap idiom falls straight out of it:

a, b = b, a

The right-hand side is evaluated first, into a tuple, and only then unpacked. No temporary variable, and no ordering bug.

Returning more than one thing

Python has no special syntax for multiple return values because it does not need any. return min(xs), max(xs) returns a tuple, and the caller unpacks it:

low, high = min_max(values)

This reads better than returning a list, because the shape is fixed: exactly two things, in a known order.

The comma is the tuple

The most common surprise is that brackets do not make a tuple — commas do.

type((5))   # int
type((5,))  # tuple

A single-element tuple needs the trailing comma. It looks like a typo and is not. This bites when a function is supposed to return one item as a tuple and quietly returns the item instead.

Unpacking in places you did not expect

Unpacking is not limited to assignment. It is how a for loop reads pairs:

for name, score in [("ana", 91), ("bo", 78)]:
    print(name, score)

Each item is a two-tuple, and the loop unpacks it into two names on the way in. dict.items() yields tuples, which is why for k, v in d.items() works at all, and enumerate yields (index, value) for the same reason.

A star collects the middle:

first, *rest = [1, 2, 3, 4]      # first = 1, rest = [2, 3, 4]
head, *body, tail = "a b c d".split()

The starred name always becomes a list, even when it captures nothing, which means rest is [] rather than None for a one-item sequence. That is worth knowing before you write if rest is None.

Where the wrong number of names bites

x, y = (1, 2, 3)

raises ValueError: too many values to unpack. That looks like an annoyance and is a safety net: it fires the moment the shape of your data changes. Code that indexed point[0] and point[1] would have carried on quietly with a three-element point and produced wrong answers much later.

The same error appears when a function you expected to return two things returns one, which is usually the real bug rather than the unpacking.

Named tuples, when positions stop being obvious

Positional access stops reading well past two or three fields. record[4] tells nobody anything.

from collections import namedtuple

Point = namedtuple("Point", "x y")
p = Point(3, 4)
print(p.x, p[0])        # both work

A named tuple is still a tuple - immutable, hashable, unpackable - but its fields have names. It costs one line and it is the usual answer when a plain tuple has grown past the point where you can remember what position two was.

Why immutability is not just a restriction

It is tempting to read "cannot be changed" as a limitation you work around. In practice it is a property you rely on. An immutable object can be shared freely between functions, threads and data structures, because no holder of it can surprise another holder by editing it. That is why the language reaches for tuples in exactly the places where a surprise would be expensive: function arguments arrive as a tuple, exceptions carry their arguments as a tuple, and a function returning several values returns a tuple rather than a list.

The immutability is shallow, and this catches people. A tuple guarantees that its slots keep pointing at the same objects; it says nothing about those objects. A tuple containing a list will happily let you append to that list, and the tuple is unchanged as far as Python is concerned, because it still points at the same list. This is also why such a tuple is not hashable: its contents can change, so any hash computed from them would go stale.

Choosing between a tuple, a list and a small class

Three options, and the decision is usually about what the positions mean.

A list is right when the items are the same kind of thing and the collection grows, shrinks or gets sorted: a list of scores, a list of users, a queue of jobs. Position carries no meaning beyond order.

A tuple is right when the group is fixed at creation and each position means something specific: a coordinate pair, an RGB colour, a database row. Position is the meaning, which is exactly why the group must not change length.

A small class or a dataclass takes over when there are more than about three fields, or when you find yourself writing a comment to remember what position three was. Names beat positions the moment positions stop being obvious, and a named tuple is the halfway house that keeps tuple behaviour while adding them.

A worked example: taking a row apart

row = ("ana", 91, "physics")

name, score, subject = row
print(f"{name}: {score} in {subject}")

name, *rest = row
print(name, rest)
ana: 91 in physics
ana [91, 'physics']

The first unpacking names all three positions, which is what you want when the shape is known and fixed — and if the row ever gains a fourth field, the ValueError tells you at once rather than letting the extra value disappear.

The second collects the tail, and the starred name is a list even though the source was a tuple. That asymmetry is deliberate: the collected part has no fixed length, so it gets the type that handles varying lengths.

The names in the first line are doing the documenting. row[1] in code three screens away means nothing; score means something. That is the real argument for unpacking early — not brevity, but that it converts positions into names at the point where you still remember what the positions were.

Tuples compare position by position

Comparison follows the same rule as sorting a dictionary word: compare the first elements, and only if they are equal look at the second.

print((1, 2) < (1, 3), (1, 2) < (2, 0), ("a", 2) < ("a", 10))

rows = [("bo", 78), ("ana", 91), ("ana", 43)]
print(sorted(rows))
True True True
[('ana', 43), ('ana', 91), ('bo', 78)]

The second comparison is the one worth noticing: (1, 2) < (2, 0) is true even though 2 is greater than 0, because the first elements already decided it. The later positions are never consulted once an earlier one differs.

This is exactly why a tuple works as a sort key. key=lambda r: (-r.score, r.name) sorts by score and breaks ties by name, and it does so because tuple comparison stops at the first difference. Every multi-level sort in the track relies on this one rule.

It also means comparison fails the same way unpacking does when the types do not line up: (1, "a") < (1, 2) raises, because once the first elements tie Python has to compare a string with an integer.

Tuples you use without noticing

A good deal of Python hands you tuples whether you asked for them or not, and recognising them explains several pieces of syntax at once.

A function returning several values returns a tuple; return a, b builds one and the caller unpacks it. *args collects into a tuple, not a list, which is why you cannot append to it inside the function. dict.items(), enumerate and zip all yield tuples, which is why for k, v in ... works for all three. An exception carries its arguments in e.args, a tuple. String formatting with % takes one, which is why "%s" % (value,) needs that trailing comma.

And the swap, a, b = b, a, builds a tuple on the right and unpacks it on the left — there is no special swap syntax, just the ordinary rules applied in both directions in one statement.

The common thread is that a tuple is Python's way of saying "a fixed group of things, in order". Wherever the language needs that, it uses one, which is why learning tuple behaviour pays off far outside the places you write the brackets yourself.

Immutable does not mean constant

Two words get used interchangeably and mean different things, and the confusion causes real bugs.

A tuple is immutable: the object cannot be changed after it is built. That is a property of the object.

A name is rebindable: point = (3, 4) followed by point = (5, 6) is perfectly legal. The first tuple was never modified; the name now refers to a different one. Nothing about immutability stops a variable from being pointed somewhere else, and Python has no way to prevent that at all — the uppercase naming convention for constants is a request, not a rule.

The practical consequence is that handing someone a tuple guarantees they cannot change *your* object. It does not guarantee that a name in their code still refers to it later, and it does not make the values inside it constant if those values are themselves mutable.

This is the same distinction as "rebinding versus mutating" from elsewhere in the track, seen from the other side. Immutability removes one of the two operations; the other one is always available.

Questions people ask

Do I need the brackets? Usually not — the comma makes the tuple. Brackets are for clarity and for cases where precedence would otherwise take over, such as inside a function call.

How do I make a one-element tuple? (5,) with the trailing comma. (5) is just the number in brackets.

Can I sort a tuple? sorted(t) returns a list. A tuple cannot be sorted in place, because that would change it.

Are tuples faster than lists? Slightly to build and slightly smaller, and that is rarely the reason to pick one. Pick by whether the contents should be able to change.

Can a tuple contain a list? Yes, and then it is not hashable — so it cannot be a dictionary key, even though it is a tuple.

What does ValueError: too many values to unpack mean? The right-hand side had more items than you gave names for. Usually the data changed shape.

Is namedtuple still worth using? Yes for a lightweight immutable record, though a frozen dataclass is often clearer for anything with behaviour attached.

Can I unpack into an existing list element? Yes — a[0], a[1] = a[1], a[0] is legal, and is how you swap two positions of a list in one statement.

Does unpacking work on a generator? Yes, and it consumes it. The count still has to match, so it will run the generator to the end to find out.

Why is *args a tuple rather than a list? Because the arguments are a fixed group by the time the function starts, and making it immutable stops a function from accidentally altering what it was called with.

Recap in one screen

  • A tuple is a fixed, ordered group whose positions carry meaning; that is what makes it hashable and safe to hand around.
  • The comma makes the tuple, not the brackets — and a one-element tuple needs the trailing comma.
  • Unpacking turns positions into names, and a wrong count raises immediately rather than failing quietly later.
  • Comparison goes position by position and stops at the first difference, which is what makes tuple sort keys work.
  • Immutability is shallow: a tuple containing a list is neither frozen nor hashable.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Why can a tuple be a dictionary key when a list cannot?

  2. What is `type((5))`?

  3. In `a, b = b, a`, why is no temporary variable needed?

Cheat sheet

Tuples and Unpacking

A tuple is a sequence that cannot be changed once built. That single restriction is what makes it usable as a dictionary key, safe to hand around, and the natural way to return more than one value.

PYTHON · vizlearn.in/python/tuples_and_unpacking.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.