Lists, tuples, dicts and sets · about 40 min

Tuples and unpacking

Why this matters

Often a few values belong together: a point's x and y, a record's name and age, the minimum and maximum of some data. A tuple groups them into a single immutable value. Tuples come with unpacking, one of Python's most pleasant features, which lets you pull the values back out into names in one line. You already used tuples without noticing: *args collects arguments into one, and a, b = b, a swaps two variables by packing and unpacking a tuple.

Tuples

A tuple is an ordered, immutable sequence. You write it as values separated by commas, usually in parentheses:

point = (3, 4)
person = ("Ada", 1815, "London")
print(point[0], person[-1], len(person))
print(person[1:])
3 London 3
(1815, 'London')

Indexing, slicing, len, in, +, *, iteration, count and index work just as for lists. What you cannot do is change a tuple:

point[0] = 10
TypeError: 'tuple' object does not support item assignment

There is no append, remove or sort either. To "change" a tuple you build a new one: point = (10,) + point[1:].

The comma makes the tuple

The parentheses are mostly for grouping and readability; the comma is what creates a tuple:

a = 1, 2, 3       # a tuple
b = (5)           # just the int 5
c = (5,)          # a one-element tuple
d = ()            # the empty tuple (the one case without a comma)
print(type(a), type(b), type(c), type(d))
<class 'tuple'> <class 'int'> <class 'tuple'> <class 'tuple'>

Why have an immutable list?

Immutability is shallow: a tuple holds references, and if one of them points to a mutable object, that object can still change:

t = ("tags", ["a", "b"])
t[1].append("c")
print(t)
('tags', ['a', 'b', 'c'])

The tuple still refers to the same two objects; it is the list that changed. (Such a tuple is not hashable.)

Packing and unpacking

Writing values separated by commas packs them into a tuple. Putting several names on the left of = unpacks a sequence into them:

record = ("Grace", "Hopper", 1906)      # packing
first, last, born = record              # unpacking
print(last, born)
Hopper 1906

Unpacking works with any iterable (lists, strings, the result of split), as long as the counts match:

host, port = "example.com:443".split(":")
x, y, z = [1, 2, 3]
a, b = "hi"
print(host, port, x + y + z, a, b)
example.com 443 6 h i

A mismatch raises ValueError: too many values to unpack (expected 2) or ValueError: not enough values to unpack (expected 3, got 2).

Swapping, and how it works

a, b = 1, 2
a, b = b, a
print(a, b)   # 2 1

The right-hand side b, a is evaluated first, completely, producing a tuple (2, 1) (CPython actually optimizes small cases to a stack swap without building the tuple). Only then are the names on the left bound. That is why no temporary variable is needed, and why the Fibonacci update a, b = b, a + b uses the old a and b on the right.

Star unpacking

A name prefixed with * in an unpacking target collects "the rest" as a list:

first, *rest = [1, 2, 3, 4]
*init, last = "abcde"
head, *middle, tail = (10, 20, 30, 40)
print(first, rest, init, last, head, middle, tail)
1 [2, 3, 4] ['a', 'b', 'c', 'd'] e 10 [20, 30] 40

Only one starred name is allowed per unpacking, and it may receive an empty list.

Unpacking in for loops

Loops unpack each item too, which is very handy for sequences of pairs:

pairs = [("Ada", 36), ("Alan", 41), ("Grace", 85)]
for name, age in pairs:
    print(name, "is", age)

The iteration module builds on this with enumerate and zip, which produce exactly such pairs.

Returning multiple values

A function returns one object, but that object can be a tuple, and unpacking at the call site makes it feel like several return values:

def divide(a, b):
    """Return (quotient, remainder) of integer division."""
    return a // b, a % b

q, r = divide(17, 5)
print(q, r)
print(divide(17, 5))
3 2
(3, 2)

The built-in divmod(17, 5) does exactly this.

Named tuples, briefly

When a tuple has more than two or three fields, person[2] is cryptic. The standard library's collections.namedtuple creates a tuple type whose fields also have names:

from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p[1], p)
x, y = p          # still a tuple: unpacking works
3 4 Point(x=3, y=4)

Named tuples are still immutable tuples, just more readable. In the object-oriented module you will meet dataclasses, the more flexible modern alternative for records.

Common mistakes

Recap

Going further: read "Tuples and Sequences" in the official tutorial (section 5.3). In the REPL, try import dis; dis.dis("a, b = b, a") and look for the SWAP instruction.

Exercises

Exercise 1 · quiz

Which is a tuple?

Which of these expressions produce a tuple? Select all that apply.

Answer
  • (5,)

  • 5, 6

  • ()

The comma makes a tuple, not the parentheses. (5) is just the integer 5 in grouping parentheses. (5,) is a one-element tuple, 5, 6 is a two-element tuple even without parentheses, and () is the special spelling of the empty tuple. [5, 6] is a list.

Exercise 2 · quiz

Star unpacking

What does this print?

first, *middle, last = [10, 20, 30, 40, 50]
print(first, middle, last)
Answer
  • 10 [20, 30, 40] 50

The starred name collects every item not taken by the other names, always as a list (even when unpacking a tuple). Without the star, unpacking five items into three names raises ValueError: too many values to unpack (expected 3).

Exercise 3 · write code

Return two values

Define min_max(numbers) that returns a tuple (smallest, largest) for a non-empty list of numbers, using a single loop (do not call min, max or sorted).

low, high = min_max([4, -2, 9, 0])   # low == -2, high == 9
min_max([7])                          # returns (7, 7)

File: solution.py

Starting point

def min_max(numbers):
    return [min(numbers), max(numbers)]
Hint 1

Start with low = high = numbers[0], then compare every other number.

Hint 2

return low, high packs both into a tuple.

Reference solution
def min_max(numbers):
    """Return (smallest, largest) of a non-empty sequence of numbers."""
    low = high = numbers[0]
    for n in numbers[1:]:
        if n < low:
            low = n
        elif n > high:
            high = n
    return low, high

Exercise 4 · write code

Tuple assignment

Read an integer n (n >= 1) and print the first n Fibonacci numbers on one line separated by single spaces, starting 0 1 1 2 3 5 ....

Use two variables a, b = 0, 1 and update both at once with tuple assignment (a, b = b, a + b); no lists are needed. For input 7:

0 1 1 2 3 5 8

File: main.py

Starting point

n = int(input())
a, b = 0, 1

Example

input

7

expected output

0 1 1 2 3 5 8

Example

input

1

expected output

0
Hint 1

Print a, then advance with a, b = b, a + b. The right side is evaluated completely before either name is rebound.

Hint 2

To avoid a trailing space, print end=" " for all but the last, or build a list of strings and " ".join it.

Reference solution
n = int(input())
a, b = 0, 1
for i in range(n):
    if i > 0:
        print(" ", end="")
    print(a, end="")
    a, b = b, a + b
print()

Exercise 5 · write code

Unpack records

Each input line has the form name,city,age. Read lines until a line containing only end, then print the name and age of the oldest person in the form <name> (<age>). If several people share the greatest age, print the first one read. Inside your loop, unpack each line with name, city, age = line.split(",").

For this input:

Ada,London,36
Grace,Arlington,85
Linus,Helsinki,54
end

the output is:

Grace (85)

File: main.py

Starting point

best = None
line = input()
while line != "end":
    line = input()

Example

input

Ada,London,36
Grace,Arlington,85
Linus,Helsinki,54
end

expected output

Grace (85)

Example

input

Bo,Oslo,9
Al,Rome,10
end

expected output

Al (10)
Hint 1

Convert the age with int(age) before comparing, or "9" > "85" will be True.

Hint 2

Keep the best record so far as a tuple (name, age) and replace it only when a strictly greater age appears.

Reference solution
best = None
line = input()
while line != "end":
    name, city, age = line.split(",")
    record = (name, int(age))
    if best is None or record[1] > best[1]:
        best = record
    line = input()
name, age = best
print(f"{name} ({age})")