This lesson introduces the tuple as an immutable sequence, covers its syntax and the singleton comma, shows which list operations carry over and which do not, explains how sequences are compared, and uses tuple assignment to swap variables and return several values at once.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 12 — Tuples
§12.1-12.3, pp. 115-117
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §12.1-12.3, pp. 115-117 — the pages these objectives are drawn from
Warm-up
The last chapter ended with a restriction and a hint.
Discussion prompt
Chapter 11 said a list cannot be a dictionary key, and that the simplest way round the limitation is a type we had not met. What must that type be like, given the reason lists were rejected?
Hint: Why were lists rejected?
Answer:
Lists were rejected because they are mutable: a key that changes after it is filed can no longer be found where it was put.
So the new type must be a sequence — otherwise it would not be a substitute — and it must be immutable, so its hash value cannot change.
That is exactly what a tuple is. You already know why it has to exist before you know what it looks like, which is a rare and pleasant position to be in.
Concept
A tuple is a sequence of values. The values can be any type, and they are indexed by integers, so in that respect tuples are a lot like lists. The important difference is that tuples are immutable.
tuple — An immutable sequence of elements.
There is no consensus on how to pronounce it. Some people say tuh-ple, which rhymes with supple; but in the context of programming, most people say too-ple, which rhymes with quadruple.
Figure (svg): Three columns comparing strings, lists and tuples by what they hold and whether they can change
Think Python, 2nd edition — Allen B. Downey §12.1-12.3, pp. 115-115
Section
Section 1
Concept
Syntactically, a tuple is a comma-separated list of values. Although it is not necessary, it is common to enclose tuples in parentheses.
>>> t = 'a', 'b', 'c', 'd', 'e'
>>> t = ('a', 'b', 'c', 'd', 'e') # the same thing
>>> t1 = 'a',
>>> type(t1)
<class 'tuple'>
>>> t2 = ('a')
>>> type(t2)
<class 'str'>| What you write | What you get | Note |
|---|---|---|
| 'a', 'b', 'c' | commas: a tuple | parentheses optional |
| 'a', | one value and a comma | a tuple with one element |
| ('a') | parentheses, no comma | just a string |
To create a tuple with a single element, you have to include a final comma. A value in parentheses is not a tuple — the parentheses are grouping, exactly as they are in arithmetic, and only the comma builds a sequence.
Think Python, 2nd edition — Allen B. Downey §12.1-12.3, pp. 115-115
Picture it
Four expressions that look similar and produce three different types.
Figure (svg): A comparison of four similar expressions and the type each produces
Read the parentheses as grouping and the comma as construction, and every one of these becomes predictable.
Worked example
The commonest tuple mistake, and it is silent.
>>> t1 = 'a',
>>> type(t1)
<class 'tuple'>
>>> len(t1)
1
>>> t2 = ('a')
>>> type(t2)
<class 'str'>
>>> len(t2)
1| Value | What it is | Note |
|---|---|---|
| t1 | a tuple holding one string | len 1 |
| t2 | a string of one character | len 1 |
| len | the same for both | which is why the bug hides |
Write the singleton with its comma.
Why: To create a tuple with a single element, you have to include a final comma — 'a', is a tuple and ('a',) is the same tuple with grouping added.
Write it without.
Why: A value in parentheses is not a tuple. ('a') is the string 'a', because the parentheses only grouped it.
Notice why this is dangerous.
Why: Both have length 1, both index to 'a', and both print recognisably. The difference shows up only when something depends on the type.
Figure (svg): The state of the program after each line of Worked example the singleton comma, drawn as a ladder with one rung per traced line
The comma makes a one-element tuple; the parentheses alone make nothing at all. len cannot tell them apart, so type is the check.
Verify: Find an operation that distinguishes them.
Why: t1 + t1 gives a two-element tuple; t2 + t2 gives the string 'aa'. Concatenation shows the difference because it produces something of the same type, which is exactly why a singleton written without its comma tends to surface far from where it was written.
Prediction
Parentheses, and no comma.
t = ('hello')
print(type(t))| Part | What it does | Result |
|---|---|---|
| the parentheses | group the expression | as in arithmetic |
| no comma | nothing constructs a tuple | so it stays a string |
| ('hello',) | with a comma | would be a tuple |
Predict first
What does this print?
Correct: <class 'str'> — a value in parentheses is not a tuple, because the parentheses only group it.
Why: The comma is what builds a tuple, and there is none here. Adding one — ('hello',) or just 'hello', — gives a one-element tuple. Nothing forbids a one-element or even an empty tuple: tuple() produces the latter, which prints as ().
Worked example
The counterpart of dict() and list(), with the same warning attached.
>>> t = tuple()
>>> t
()
>>> t = tuple('lupins')
>>> t
('l', 'u', 'p', 'i', 'n', 's')| Call | What it produces | Note |
|---|---|---|
| tuple() | no argument | an empty tuple, printed () |
| tuple('lupins') | a sequence argument | one element per character |
| the name | a built-in function | avoid it as a variable name |
Call it with nothing.
Why: With no argument, it creates an empty tuple, which prints as a bare pair of parentheses.
Call it with a sequence.
Why: If the argument is a sequence — string, list or tuple — the result is a tuple with the elements of that sequence.
Heed the warning.
Why: Because tuple is the name of a built-in function, you should avoid using it as a variable name — the same caution the book gave for dict and list.
Figure (svg): A pipeline showing a list being converted to a tuple and used as a dictionary key
An empty tuple, and a six-element tuple of characters. The conversion works from any sequence, which makes tuple() the way to freeze a list.
Verify: Convert a list and check it is really a tuple.
Why: tuple([1, 2, 3]) gives (1, 2, 3), and type confirms it. That conversion is the practical route to a dictionary key: build the sequence as a list, then freeze it with tuple() when you need to use it as a key.
Trap
A function should return a tuple of one item, so it returns ('result').
Add parentheses to make it a tuple
Why: Which is how every other tuple in the program is written.
The parentheses group and do not construct, so the function returns a string. The caller loops over it and gets one character per pass instead of one item, which produces plausible nonsense rather than an error.
The comma is the tuple.
Write ('result',) with the trailing comma
Why: Or 'result', without parentheses — both are the same one-element tuple.
Check with type when a singleton misbehaves
Why: len is the same for both, so it cannot tell you anything.
The mnemonic is that the parentheses are optional everywhere and the comma never is. If you can delete a piece of punctuation and still have a tuple, it was not the piece doing the work.
Sorting
Look for the comma.
Sort into buckets
For each expression, is the result a tuple?
Faded example
One character is doing all the work.
Fill in the blanks
t = ('a',)
print(type(t)) # <class 'tuple'>
Why: Without the comma the parentheses group a string and the type is str. The trailing comma is what makes it a sequence of one — and since len is 1 either way, type is the only check that distinguishes them.
Explain it to yourself
It seems arbitrary until you consider what else the parentheses do.
Discussion prompt
Why can't the parentheses be what makes a tuple? Think about what (2 + 3) * 4 would mean if they could.
Hint: Parentheses already have a job.
Answer:
Parentheses already mean grouping, everywhere in the language. If (2 + 3) built a one-element tuple, arithmetic would stop working.
So the language needed a piece of punctuation that was not already spoken for, and the comma — which already means and then another in argument lists — was the natural choice.
Which is why the parentheses around a tuple are optional and the comma is not. The parentheses are there for readability, and for the places where a bare comma would be ambiguous, such as inside a function call.
Section
Section 2
Concept
Most list operators also work on tuples. The bracket operator indexes an element and the slice operator selects a range. But if you try to modify one of the elements, you get an error.
>>> t = ('a', 'b', 'c', 'd', 'e')
>>> t[0]
'a'
>>> t[1:3]
('b', 'c')
>>> t[0] = 'A'
TypeError: object doesn't support item assignment| Expression | What happens | Result |
|---|---|---|
| t[0] | indexing works | 'a' |
| t[1:3] | slicing works, and gives a tuple | ('b', 'c') |
| t[0] = 'A' | item assignment | TypeError |
Because tuples are immutable, you can't modify the elements. But you can replace one tuple with another — which is exactly the situation strings have been in since chapter 8.
Think Python, 2nd edition — Allen B. Downey §12.1-12.3, pp. 116-116
Picture it
Everything that reads works. Everything that writes does not.
Figure (svg): Two columns separating tuple operations that work from those that raise
That is the same line that separated the two halves of lesson 10a's table — only now one side of it is unavailable rather than merely different.
Worked example
You cannot change an element. You can build a tuple that differs.
>>> t = ('a', 'b', 'c', 'd', 'e')
>>> t = ('A',) + t[1:]
>>> t
('A', 'b', 'c', 'd', 'e')| Part | What it produces | Note |
|---|---|---|
| ('A',) | a one-element tuple | note the comma |
| t[1:] | everything after the first | ('b', 'c', 'd', 'e') |
| the concatenation | a NEW tuple | nothing was modified |
| t = ... | the name is repointed | the old tuple is discarded |
Build the replacement piece.
Why: ('A',) is a one-element tuple — the comma matters, and without it the concatenation would fail with a TypeError about mixing a string and a tuple.
Take the rest by slicing.
Why: t[1:] gives everything from index 1 onward, as a new tuple.
Join and reassign.
Why: This statement makes a new tuple and then makes t refer to it.
Figure (svg): A state diagram showing the name t moved from the original tuple to a new one
('A', 'b', 'c', 'd', 'e'). No tuple was modified — a new one was built and the name was moved to it.
Verify: Ask what happened to the original.
Why: It still exists, unchanged, until nothing refers to it. If another name had been pointing at it, that name would still see ('a', 'b', ...) — which is the opposite of what happens with a list, and is precisely why immutable objects are safe to share.
Prediction
Slicing a tuple.
t = ('a', 'b', 'c', 'd')
print(t[1:3])| Part | What happens | Result |
|---|---|---|
| slicing | reads, does not modify | allowed |
| the result | a new tuple | ('b', 'c') |
| the original | untouched | as always with slicing |
Predict first
What does this print?
Correct: ('b', 'c') — slicing reads rather than modifies, so it works, and it produces a tuple.
Why: The slice operator selects a range of elements and creates a new object, which never requires modifying the original — so immutability is no obstacle. Note that the result is a tuple rather than a list: slicing a sequence gives you the same kind of sequence back, which is also true of strings.
Worked example
The restriction chapter 11 imposed, satisfied.
>>> d = {}
>>> d[(1, 2)] = 'point'
>>> d[(1, 2)]
'point'
>>> d[[1, 2]] = 'oops'
TypeError: unhashable type: 'list'| Key | Why | Result |
|---|---|---|
| a tuple key | immutable, so hashable | works |
| a list key | mutable, so unhashable | TypeError |
| why | a key's hash must not change | chapter 11's argument |
Recall the requirement.
Why: Keys have to be hashable, because a dictionary computes where to file a pair from the key's contents.
Check the tuple against it.
Why: A tuple's contents cannot change, so its hash value is fixed for as long as it exists and the pair can always be found where it was put.
Note what this unlocks.
Why: Any sequence can now be a key, by converting it with tuple() — coordinates, pairs of names, a hand of cards.
Figure (svg): The state of the program after each line of Worked example why this makes tuples good keys, drawn as a ladder with one rung per traced line
The tuple works and the list does not, for exactly the reason chapter 11 gave. Immutability is not a limitation here — it is the qualification.
Verify: Test the argument's premise on a nested case.
Why: A tuple containing a list, such as (1, [2]), is still unhashable — because its contents can change after all. That confirms the rule is about whether anything reachable can change, not about the outer type's name, which is a sharper statement than tuples can be keys.
Trap
A program has a tuple of numbers and calls t.sort() to order them.
Use the method you would use on a list
Why: sort is how you order a sequence, and a tuple is a sequence.
It raises AttributeError: tuples have no sort method, because sorting in place would modify the object. The habit transfers and the method does not.
Use sorted, which returns rather than modifies.
sorted(t) gives a new sorted LIST
Why: Not a tuple — sorted always returns a list, whatever it was given.
Wrap it if you need a tuple back
Why: tuple(sorted(t)) sorts and refreezes.
The general rule is that every modifying list method is absent from tuples and every returning built-in still works. sorted, min, max, sum, len and in all apply; append, sort, remove and del do not.
Error analysis
Mark each and say whether it raises.
Annotate
Unlike chapter 10's list mistakes, these all announce themselves — which is one practical advantage of an immutable type.
Comparison
Fill the blanks. Two rows are about contents and two about change.
Comparison matrix
| Question | List | Tuple | String |
|---|---|---|---|
| What can it hold? | any values | any values | characters only |
| Mutable? | yes | no | no |
| Can be a dictionary key? | no | yes | yes |
| Does t[0] = x work? | yes | no — TypeError | no — TypeError |
The tuple fills the gap in the table: an immutable sequence that can hold anything. Strings and lists each had one of those properties, and neither had both.
Explain it
Immutability sounds like a missing feature until you see what it buys.
Discussion prompt
A classmate asks why they would ever choose a tuple over a list, when a list can do more. Give them two reasons.
Hint: One is about dictionaries and one is about safety.
Answer:
First: only immutable things can be dictionary keys, so a tuple is the only way to index a dictionary by a pair or a sequence — coordinates, a name and a date, anything compound.
Second: something that cannot change is safe to share. Chapter 10's aliasing bugs are impossible for tuples, because there is no way for one holder to affect another.
And there is a third, softer reason: a tuple in a signature says this will not be modified, which a list cannot say. It is a promise the type system keeps for you rather than one you have to document.
Section
Section 3
Concept
The relational operators work with tuples and other sequences. Python starts by comparing the first element from each sequence. If they are equal, it goes on to the next elements, and so on, until it finds elements that differ.
>>> (0, 1, 2) < (0, 3, 4)
True
>>> (0, 1, 2000000) < (0, 3, 4)
True| Step | The comparison | What happens |
|---|---|---|
| first elements | 0 and 0: equal | go on |
| second elements | 1 and 3: differ | 1 < 3, so True |
| third elements | never examined | even 2000000 |
Subsequent elements are not considered, even if they are really big. The comparison stops at the first difference, which is why a huge number later in the tuple has no effect on the answer.
Think Python, 2nd edition — Allen B. Downey §12.1-12.3, pp. 116-116
Picture it
Two tuples, scanned left to right, until they disagree.
Figure (svg): Two tuples aligned element by element with the deciding position marked
This is exactly how words are ordered in a dictionary — the printed kind. cat comes before cot because of the second letter, and nothing later matters.
Worked example
Two million loses to a four, and the reason is positional.
>>> (0, 1, 2000000) < (0, 3, 4)
True
>>> (0, 3, 1) < (0, 3, 4)
True
>>> (1, 0, 0) < (0, 9, 9)
False| Comparison | Where it was decided | Why |
|---|---|---|
| first pair | decided at position 1 | 1 < 3 |
| second pair | positions 0 and 1 equal | decided at position 2 |
| third pair | decided at position 0 | 1 > 0, so False |
Scan from the left.
Why: Python compares the first element from each sequence, and moves on only while they are equal.
Stop at the first difference.
Why: That difference decides the answer, and subsequent elements are not considered.
Notice earlier positions dominate completely.
Why: In the third comparison a 1 at position 0 outweighs everything else, however large the later numbers are.
Figure (svg): The state of the program after each line of Worked example why the big number does not win, drawn as a ladder with one rung per traced line
True, True, False. Position matters more than magnitude: an earlier element decides the comparison outright.
Verify: Check the case where one tuple runs out.
Why: (0, 1) < (0, 1, 2) is True: if every compared element is equal and one sequence ends, the shorter one is smaller. That is the same rule that puts car before cart in a printed dictionary, and it makes the ordering complete rather than leaving pairs undecided.
Prediction
The second elements differ.
print((5, 1, 9) < (5, 2, 0))| Step | The comparison | Effect |
|---|---|---|
| position 0 | 5 and 5: equal | continue |
| position 1 | 1 and 2: differ | 1 < 2 |
| position 2 | never examined | 9 vs 0 is irrelevant |
Predict first
What does this print?
Correct: True — the comparison is decided at position 1, where 1 is less than 2.
Why: Python compares element by element until it finds a difference, and stops there. The 9 in the left tuple and the 0 in the right are never examined, because the answer was already determined. Position beats magnitude, which is the rule the book illustrates with two million losing to a three.
Worked example
The comparison rule is what makes tuples useful for sorting.
scores = [(3, 'ann'), (1, 'bob'), (3, 'amy')]
print(sorted(scores))
# [(1, 'bob'), (3, 'amy'), (3, 'ann')]| Position | What it orders by | Note |
|---|---|---|
| the first element | the score | the primary ordering |
| ties | compared on the second element | the name |
| the result | by score, then alphabetically | from one sort call |
Put the primary key first.
Why: Comparison starts at position 0, so whatever is there dominates the ordering.
Put the tie-breaker next.
Why: Two tuples with equal first elements move on to the second, which sorts amy before ann.
Get both from a single sort.
Why: No special options are needed — the element-by-element rule does the whole job.
Figure (svg): A ladder showing three score-name tuples resolved into sorted order
Sorted by score and then alphabetically within each score, from one call to sorted. Ordering by several keys is just ordering tuples.
Verify: Reverse the order inside each tuple and re-sort.
Why: With ('ann', 3) the list sorts by name first and score only within a name — a completely different result from the same data and the same sort call. That confirms position is what determines priority, and it is why the order of elements in a sorting tuple is a decision rather than a detail.
Trap
A student reads (0, 1, 2000000) < (0, 3, 4) as False, on the grounds that the left tuple obviously contains more.
Compare the tuples by their contents overall
Why: Which is how you would compare two piles of things.
Sequence comparison is positional, not aggregate. Python finds the first position where they differ and stops there, so the two million is never looked at.
Read left to right and stop at the first disagreement.
Compare position 0, then 1, and so on
Why: Exactly as words are alphabetised.
If you want an aggregate comparison, compute one
Why: sum(t1) < sum(t2) asks the question you meant, and it is a different question.
The book makes the point explicitly — subsequent elements are not considered, even if they are really big — because the aggregate reading is the natural one and it is wrong.
Ranking
Smallest first, by the element-by-element rule.
Put in order
Why: All three tuples beginning with 1 come before the one beginning with 2, whatever follows. Among them, (1,) is shortest and runs out first, which makes it smallest; then 9 before 100 at position 1. The 100 never helps, because position 0 was already decided against it relative to (2, 0).
Faded example
The tuple's order decides the sort's priority.
Fill in the blanks
pairs = [(score, name) for ...]
ranked = sorted(pairs) # by score, ties broken by name
Why: sorted compares the tuples element by element, so position 0 — the score — dominates and position 1 breaks ties. No key function or special option is needed: the ordering falls out of how sequences compare. Putting name first instead would sort alphabetically and use the score only within a name.
Real world
You have used this rule for years without naming it.
Discussion prompt
Where outside programming do you compare two things by scanning left to right and stopping at the first difference?
Hint: Any ordered list of words or numbers.
Answer:
Alphabetical order is exactly this rule. cat before cot is decided at the second letter, and nothing after it is consulted.
So are dates written year-month-day, phone directories, version numbers, and library shelf marks — all designed so that the most significant part comes first and comparison can stop early.
That design is deliberate: putting the dominant field first is what makes a simple left-to-right scan produce the ordering you want. Choosing the order of a sorting tuple is the same decision, made in code.
Section
Section 4
Concept
It is often useful to swap the values of two variables. With conventional assignments you have to use a temporary variable, which is cumbersome; tuple assignment is more elegant.
>>> temp = a
>>> a = b
>>> b = temp
>>> a, b = b, a # the same swap| Part | What it is | Note |
|---|---|---|
| the left side | a tuple of variables | the targets |
| the right side | a tuple of expressions | the values |
| the order | all expressions evaluated first | then all assignments |
The left side is a tuple of variables and the right side is a tuple of expressions. Each value is assigned to its respective variable — and, crucially, all the expressions on the right side are evaluated before any of the assignments.
Think Python, 2nd edition — Allen B. Downey §12.1-12.3, pp. 116-117
Picture it
Two phases, and the second cannot disturb the first.
Figure (svg): A flowchart showing both right-hand expressions evaluated before either assignment happens
Without that rule, assigning to a would destroy the value b needs — which is precisely the problem the temporary variable was solving.
Worked example
Two variables exchange values with no temporary.
>>> a = 1
>>> b = 2
>>> a, b = b, a
>>> a
2
>>> b
1| Step | What happens | Note |
|---|---|---|
| evaluate b | 2 | held |
| evaluate a | 1 | held |
| assign to a | a becomes 2 | the held 1 is untouched |
| assign to b | b becomes 1 | from the held value |
Evaluate the whole right side.
Why: All the expressions on the right side are evaluated before any of the assignments — so both old values are in hand before anything changes.
Assign in order.
Why: Each value is assigned to its respective variable, left to right.
See why no temporary is needed.
Why: The held values are not affected by the assignments, so overwriting a cannot destroy what b is about to receive.
Figure (svg): The state of the program after each line of Worked example the swap, step by step, drawn as a ladder with one rung per traced line
a is 2 and b is 1. The rule about evaluation order is doing the work that temp used to do.
Verify: Try the same swap written as two ordinary assignments.
Why: a = b followed by b = a leaves both variables holding 2, because the first assignment destroyed the value the second needed. Comparing the two makes it clear that the swap's elegance comes from the evaluation rule and not from the syntax.
Prediction
All the right-hand expressions are evaluated first.
a = 1
b = 2
a, b = b, a
print(a, b)| Step | What happens | Result |
|---|---|---|
| evaluate the right | b is 2, a is 1 | both held |
| assign to a | a becomes 2 | the held 1 survives |
| assign to b | b becomes 1 | from the held value |
Predict first
What does this print?
Correct: 2 1 — the values are exchanged, because both right-hand expressions are evaluated before either assignment happens.
Why: The evaluation rule is what makes this work. Writing it as two statements — a = b then b = a — gives 2 2, because the first assignment destroys the value the second needs. That comparison is the clearest demonstration that the rule, not the syntax, is doing the work.
Worked example
The right side can be any sequence, not only a tuple.
>>> addr = 'monty@python.org'
>>> uname, domain = addr.split('@')
>>> uname
'monty'
>>> domain
'python.org'
>>> a, b = 1, 2, 3
ValueError: too many values to unpack| Part | What happens | Note |
|---|---|---|
| split('@') | a list of two elements | the return value |
| two names on the left | matched to two elements | in order |
| a mismatch | two names, three values | ValueError |
Note what split returns.
Why: The return value from split is a list with two elements — the first is assigned to uname and the second to domain.
Note that the type does not matter.
Why: More generally, the right side can be any kind of sequence: string, list or tuple.
Note the one requirement.
Why: The number of variables on the left and the number of values on the right have to be the same, or you get a ValueError.
Figure (svg): A pipeline showing an address split into a list and unpacked into two names
Two names bound to the two halves of the address, in one line. The mismatch case fails loudly rather than binding some and ignoring the rest.
Verify: Try unpacking a string.
Why: a, b, c = 'xyz' binds the three characters, because a string is a sequence too. That generality is worth knowing: unpacking is about the number of elements, not about which sequence type produced them.
Trap
A program unpacks the result of split into two names, and one input contains two separators.
Assume the input has the expected shape
Why: It does for every example the developer tried.
split returns three elements and the assignment raises ValueError: too many values to unpack. The program worked on all the test data and fails on the first awkward record.
Make the count certain, or check it.
Limit the split
Why: addr.split('@', 1) returns at most two pieces, whatever the input contains.
Or unpack into a list and check its length
Why: Which lets you report a clear error instead of a ValueError from deep inside.
The failure is at least loud — the counts must match, and Python says so. That is better than the silent alternative of binding the first two and dropping the rest, which is what a language without this check would do.
Invariant
Step through and watch when each variable changes.
Step through it
Between which two frames would a temporary variable have been needed, if the values were not held?
Between the third and fourth: assigning to a would have destroyed the 1 that b needs. Holding both values first is exactly what temp used to do, and the language does it for you.
Faded example
One line to take an address apart.
Fill in the blanks
addr = 'monty@python.org'
uname, domain = addr.split('@')
print(domain) # python.org
Why: split returns a list of two elements, and the two names on the left are bound to them in order. The right side can be any sequence — string, list or tuple — but the counts must match, or the assignment raises ValueError: too many values to unpack.
Socratic
The rule looks like a detail and it is the whole mechanism.
Discussion prompt
Suppose Python assigned each variable as soon as its value was computed, left to right. What would a, b = b, a produce, and why?
Hint: Work out what b would be assigned from.
Answer:
a would be assigned b's value first, so a becomes 2. Then b would be assigned a's value — but a is now 2, so b becomes 2 as well.
Both variables would end up holding 2 and the original 1 would be lost. That is exactly the bug the temporary variable existed to prevent.
So evaluating everything before assigning anything is not a nicety; it is what makes simultaneous assignment mean something. Once you see that, the swap stops being a trick and becomes an obvious consequence of the rule.
Section
Section 5
Concept
Strictly speaking, a function can only return one value — but if the value is a tuple, the effect is the same as returning multiple values.
def min_max(t):
return min(t), max(t)
>>> min_max([3, 1, 4, 1, 5])
(1, 5)
>>> low, high = min_max([3, 1, 4, 1, 5])
>>> low
1| Part | What happens | Note |
|---|---|---|
| return min(t), max(t) | the comma makes a tuple | one value returned |
| received as one name | a tuple | (1, 5) |
| received as two names | tuple assignment unpacks it | low and high |
max and min are built-in functions that find the largest and smallest elements of a sequence. min_max computes both and returns a tuple of two values — and the caller chooses whether to take it as one thing or as two.
Think Python, 2nd edition — Allen B. Downey §12.1-12.3, pp. 117-117
Picture it
The function does the same thing; the call site decides the shape.
Figure (svg): Two columns showing the same returned tuple received as one name or unpacked into two
This is why §12.2 comes before §12.3 in the book: unpacking is what makes returning a tuple feel like returning several values.
Worked example
Two results from one computation.
>>> t = divmod(7, 3)
>>> t
(2, 1)
>>> quot, rem = divmod(7, 3)
>>> quot
2
>>> rem
1| Form | What you get | Note |
|---|---|---|
| divmod(7, 3) | quotient and remainder | as a tuple |
| stored as t | one name holding both | (2, 1) |
| unpacked | two names | 2 and 1 |
Note the inefficiency it avoids.
Why: If you want to divide two integers and compute the quotient and remainder, it is inefficient to compute x//y and then x%y — it is better to compute them both at the same time.
Note what it returns.
Why: The built-in function divmod takes two arguments and returns a tuple of two values, the quotient and the remainder.
Note the caller's choice.
Why: You can store the result as a tuple, or use tuple assignment to store the elements separately.
Figure (svg): The state of the program after each line of Worked example divmod, and why it exists, drawn as a ladder with one rung per traced line
(2, 1), received either way. The tuple is the mechanism that lets one return statement deliver two results.
Verify: Check the two results against the division.
Why: 7 divided by 3 is 2 with 1 left over, and 2 * 3 + 1 is 7 — a consistency check of exactly the kind lesson 11c described. It also confirms the order: quotient first, remainder second, which is worth fixing in memory since unpacking them the wrong way round is silent.
Prediction
One return statement, two expressions.
def min_max(t):
return min(t), max(t)
print(min_max([3, 1, 4, 1, 5]))| Part | What it computes | Result |
|---|---|---|
| min(t) | the smallest | 1 |
| max(t) | the largest | 5 |
| the comma | builds a tuple | (1, 5) |
Predict first
What does this print?
Correct: (1, 5) — the comma makes a tuple, so the function returns one value that happens to hold two.
Why: Strictly speaking a function can only return one value, and the tuple is how it delivers two. Printing it shows the tuple's own form, with parentheses and a comma. Writing low, high = min_max(...) instead would unpack it into two names, which is the usual way to receive it.
Worked example
The comma in the return statement is the whole trick.
def min_max(t):
return min(t), max(t)
def first_last(t):
return t[0], t[-1]
>>> low, high = min_max([3, 1, 4])
>>> a, z = first_last('python')| Part | What happens | Note |
|---|---|---|
| the comma | builds a tuple | one value returned |
| no parentheses needed | the comma is enough | though they are common |
| the caller | unpacks with tuple assignment | two names |
Separate the results with a comma.
Why: return min(t), max(t) returns one value — the tuple (min, max) — because the comma builds it.
Leave the parentheses off or put them on.
Why: return (min(t), max(t)) is identical. The comma is doing the work either way.
Let the caller decide the shape.
Why: They can take one name or unpack into two, and the function does not need to know which.
Figure (svg): A call diagram showing a function returning a tuple that the caller unpacks into two names
Two functions each returning a pair. Nothing special is needed in the return statement beyond a comma.
Verify: Check what happens if the caller unpacks into the wrong number of names.
Why: a, b, c = min_max(t) raises ValueError: not enough values to unpack. So the function's shape is enforced at the call site rather than silently mismatched — the same counting rule as any other tuple assignment.
Trap
A caller writes rem, quot = divmod(a, b), reasoning that the remainder is the interesting part.
Name the variables in the order you care about
Why: Which has no bearing on the order the function returns them in.
The names are bound by position, not by meaning, so quot holds the remainder and vice versa. Nothing raises, and every calculation afterwards is wrong.
Match the order the function returns.
Check the documentation or the return statement
Why: divmod returns the quotient first — the name says so, div before mod.
Sanity-check the values once
Why: For divmod(7, 3), the quotient is 2 and the remainder 1; if your names disagree, they are swapped.
This is a silent failure in a chapter otherwise full of loud ones, and it is worth a moment's care: tuple assignment binds strictly by position and cannot know what you meant.
Discrimination
Ask what the call site ends up holding.
Sort into buckets
For each line, does the caller end up with one name or several?
Faded example
The comma is all that is needed.
Fill in the blanks
def min_max(t):
return min(t), max(t)
Why: The comma builds a tuple, so the function returns one value holding both results — and a caller can unpack it into two names with tuple assignment. Parentheses around the pair are optional and change nothing; as everywhere else in this lesson, the comma is what constructs.
Explain it
It can. The resolution is worth stating precisely.
Discussion prompt
A classmate is confused because min_max seems to return two values when they were told a function returns one. Resolve it for them.
Hint: How many objects came back?
Answer:
It does return one value: a tuple. The comma in the return statement built a single object that happens to contain two things, and one object came back.
What makes it feel like two is what happens at the call site: tuple assignment immediately takes the pair apart into two names, so the caller never handles the tuple as such.
So the rule is intact and the convenience is real. It is worth saying that way round, because it explains why low, high = f() works for anything f returns that has two elements — a tuple, a list, even a two-character string.
Comparison
Fill the blanks. Everything follows from one row.
Comparison matrix
| Question | List | Tuple |
|---|---|---|
| How is it written? | square brackets | commas, with optional parentheses |
| Can an element be replaced? | yes — t[0] = x | no — TypeError |
| How do you get a modified version? | modify it in place | build a new one and repoint the name |
| Can it be a dictionary key? | no — unhashable | yes — its hash cannot change |
The second row is the difference and the rest are consequences. Reading operations transfer unchanged; writing operations do not exist.
Pattern
Five steps, and the last one is where the silent bug lives.
Step 4's count is checked and step 5's order is not. A wrong count raises ValueError; a wrong order produces confident nonsense.
Python documentation — Data Structures Data Structures
Check
One of these is a tuple.
a = ('x')
b = ('x',)
print(type(a) == type(b))| Expression | What it builds | Type |
|---|---|---|
| ('x') | parentheses only | a string |
| ('x',) | a comma | a tuple of one |
| comparing types | str against tuple | False |
Check your understanding
What does this print?
Answer: A
Why: a is a string and b is a one-element tuple, so their types differ. A value in parentheses is not a tuple; to create a tuple with a single element you have to include a final comma. Both have length 1, which is why this mistake usually surfaces somewhere other than where it was made.
Check
One of these succeeds.
Check your understanding
Given t = ('a', 'b', 'c'), which line leaves t as ('A', 'b', 'c')?
Answer: B
Why: You cannot modify a tuple's elements, but you can replace one tuple with another: this statement makes a new tuple and then makes t refer to it. The singleton needs its comma, and the slice supplies the rest.
Check
Two names, three values.
a, b = 1, 2, 3| Part | How many | Effect |
|---|---|---|
| the left side | two variables | two targets |
| the right side | three values | one too many |
| the result | ValueError | counts must match |
Check your understanding
What happens?
Answer: B
Why: The number of variables on the left and the number of values on the right have to be the same. Python does not silently discard, group, or reorder — it raises, which means a shape mismatch is found at the assignment rather than several steps later.
Real world
A fixed-shape record is a different thing from a list of items.
Discussion prompt
Think of some data with a fixed number of parts in a fixed order — a date, a coordinate, a name and a score. What would go wrong if you treated it as a list that anyone could lengthen?
Hint: What does position mean in each case?
Answer:
In a fixed record each position means something specific: the second element of a coordinate is the y value, not merely the second thing. In a list, position usually means only order.
If it could be lengthened or reordered, every piece of code that reads position 1 would be wrong, and nothing would announce it. The shape is part of the meaning.
That is the everyday case for tuples: a list is a collection of similar things whose length varies, and a tuple is a record of dissimilar things whose shape does not. Choosing between them says which kind of data you have.
Commit first
Answer, then rate your confidence. This one costs people an afternoon.
Predict first
What is the type of ('a')?
Correct: str — a value in parentheses is not a tuple, because the parentheses only group it.
Why: The comma is what builds a tuple, not the parentheses. ('a') is the string 'a' with redundant grouping, exactly as (2) is the integer 2. To make a one-element tuple you need the final comma: ('a',) or just 'a',. This is the chapter's most expensive trap because nothing raises and len reports 1 for both — so a function that was supposed to return a one-element tuple returns a string, the caller loops over it, and gets one character per pass instead of one item. The check is type, since len cannot distinguish them.
Explain it
Three sequence types, and where the new one fits.
Discussion prompt
A classmate asks what a tuple is for, given that lists exist. Place it in the table of sequence types and give the two things only it can do.
Hint: Cross two properties: what it holds, and whether it can change.
Answer:
Draw the grid: strings hold characters and cannot change; lists hold anything and can change; tuples hold anything and cannot change. The tuple fills the empty square.
The first thing only a tuple can do is be a dictionary key while holding a sequence of arbitrary values — because its hash cannot change, which is exactly chapter 11's requirement.
The second is to be handed around safely. A tuple cannot be modified by anyone who receives it, so none of chapter 10's aliasing surprises are possible — the immutability is a guarantee rather than a restriction.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: The syntax is where nearly everyone is caught once, and the singleton comma is worth over-learning because its failure is silent. Which operations survive follows entirely from immutability, so it is more a rule to derive than to memorise. The comparison rule matters most when you start sorting by several keys. And tuple assignment is the part you will use every day — the evaluation-order rule behind the swap is the piece that turns it from a trick into something predictable.
Connect it up
One page, from memory.
Draw it
Draw a three-by-two grid with strings, lists and tuples down one side and what it holds and can it change across the top, and fill it in. Beside it, write the four ways to build a tuple and mark which one is not a tuple at all. Underneath, write the swap statement and label the two phases the interpreter goes through, then write a function that returns two values and the two ways a caller can receive them.
Recap
Three pages, and the sequence type that completes the set.
| If you remember one thing | It is this |
|---|---|
| From the syntax | The comma makes the tuple. ('a') is a string. |
| From immutability | Reading operations transfer from lists; writing operations do not exist. |
| From comparison | Position beats magnitude — the first difference decides everything. |
| From tuple assignment | The whole right side is evaluated before any assignment happens. |
| From return values | A function returns one value; a tuple is how it delivers several. |
The next lesson uses the gather and scatter operators to write functions that take any number of arguments, and introduces zip and enumerate — the tools for traversing two sequences at once, or a sequence and its own indices.
Think Python, 2nd edition — Allen B. Downey §12.1-12.3, pp. 115-117 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.