6.1 Tuples

A pair of coordinates or a returned count and total has a fixed order. A tuple stores such a group of values as an ordered sequence. You can read its elements by position, as with a list, while its entries remain fixed after creation.

6.1.1 Creating and reading tuples

Commas separate a tuple's elements. Parentheses make the grouping visible, as in (2, 5). A tuple can contain numbers, strings, or other collections. Table 1 shows empty, single-element, mixed, and nested tuples. The Python tuple reference explains the comma-based construction rules.

Table 1: Examples of tuples in Python.
Tuple Example Description
("apple", "banana", "cherry") A tuple of strings.
(1, 2, 3, 4, 5) A tuple of integers.
("apple", 1, "banana", 2) A tuple of mixed data types.
("apple", ("banana", "cherry"), "orange") A tuple with a nested tuple.
() An empty tuple.
((1, 2), (3, 4), (5, 6)) A 2D tuple with three rows and two columns.
(1,) A tuple with a single element. Note the comma , after the element.

The comma in (1,) makes it a single-element tuple. (1) is the integer 1 enclosed in parentheses. Write () for an empty tuple, or use tuple(). tuple([1, 2, 3]) converts a list to the tuple (1, 2, 3).

Indexing and slicing follow the rules already introduced for lists. Listing 1 reads individual elements and a slice. Indexing begins at zero, a negative index counts backward from the end, and a slice excludes its ending index.

Listing 1: Read positions and a slice from a tuple of three fruit names.
fruits = ("apple", "banana", "cherry")
print(fruits[0])
print(fruits[-1])
print(fruits[1:3])
print(len(fruits))

The output is apple, cherry, ('banana', 'cherry'), and 3 on separate lines. The slice produces a tuple. A nested tuple can be read with successive indices; ((1, 2), (3, 4))[1][0] gives 3.

6.1.2 Tuple packing and unpacking

Packing collects values into a tuple. The assignment fruits = "apple", "banana", "cherry" packs three values, with optional parentheses. Tuple unpacking assigns the elements to separate variables in their sequence order. Listing 2 shows both operations.

Listing 2: Tuple packing and unpacking in Python.
# Tuple packing
fruits = "apple", "banana", "cherry"

# Tuple unpacking
fruit1, fruit2, fruit3 = fruits

print(fruit1)  # Output: apple
print(fruit2)  # Output: banana
print(fruit3)  # Output: cherry
apple
banana
cherry

The three printed names are apple, banana, and cherry. The number of variables must match the number of elements in this form of unpacking. For example, first, second = (1, 2, 3) raises ValueError because three values cannot be assigned to two variables.

Unpacking also explains the familiar loop for index, value in enumerate(values): each generated pair supplies one value for index and one for value. A function can return several values as a tuple, such as return count, total; its caller can receive them with count, total = summarize(values).

6.1.3 Tuple immutability

A tuple is immutable. You cannot replace, add, or remove its entries after creation. Listing 3 attempts to replace the first entry and catches the resulting error so you can inspect the unchanged tuple.

Listing 3: Attempt to replace an entry in a tuple and inspect the unchanged tuple.
point = (2, 5)
try:
    point[0] = 3
except TypeError as error:
    print("TypeError:", error)
print(point)

Python prints TypeError: 'tuple' object does not support item assignment and then (2, 5). You can assign point = (3, 5) afterward; that assignment makes the variable refer to a different tuple.

A tuple can contain a mutable object such as a list. The tuple's entry continues to refer to that list, whose contents can change. Listing 4 shows the distinction.

Listing 4: Change a list stored inside a tuple while leaving the tuple's entries fixed.
delivery = ("Monday", [2, 3])
delivery[1].append(4)
print(delivery)
assert delivery == ("Monday", [2, 3, 4]), "Check the contained list"

The result is ('Monday', [2, 3, 4]). The list changed; replacing delivery[1] with another list would still raise TypeError.

6.1.4 Returning a value and its identifier

When you find the largest value in a sequence, you may also need to know which item supplied it. Store the value and its identifier in one tuple, then replace that tuple whenever you find a larger value. Listing 5 finds the largest measured package mass.

Listing 5: Find the largest package mass in kilograms and keep its identifier in the same tuple.
packages = [("P01", 3.2), ("P02", 4.5), ("P03", 4.5)]
package_id, mass = packages[0]
best = (mass, package_id)
index = 1

while index < len(packages):
    package_id, mass = packages[index]
    if mass > best[0]:
        best = (mass, package_id)
    index += 1

print(best)
assert best == (4.5, "P02"), "Check the largest mass and its identifier"

The initial tuple is (3.2, "P01"). After visiting P02, it is (4.5, "P02"). The comparison uses >, so the equal mass of P03 leaves the first matching package in the result. The code reads the input list without changing it and assumes at least one package.

For several measurements per item, repeat the search for each measurement position. An outer loop selects the position, and an inner loop visits every item. Collect one result tuple per position in a new list.

6.1.5 Further study

The following operations extend the creation, reading, and unpacking above. Named tuples and zip are useful additional ways to organize related values.

Tuple methods and functions

Tuples have the methods count() and index(). Built-in functions such as len() and sum() also accept tuples. A method is called through an object, as in t.count(1); a function receives the object as an argument, as in len(t). Table 2 keeps that distinction visible for t = (1, 2, 3, 1, 2).

Table 2: Methods and functions applied to the tuple t = (1, 2, 3, 1, 2).
Method or function Description Example Result of the example
tuple.count(element) Returns the number of occurrences of the specified element in the tuple. t.count(1) 2
tuple.index(element) Returns the index of the first occurrence of the specified element in the tuple. t.index(2) 1
len(tuple) Returns the number of elements in the tuple. len(t) 5
max(tuple) Returns the maximum element in the tuple. max(t) 3
min(tuple) Returns the minimum element in the tuple. min(t) 1
sum(tuple) Returns the sum of elements in the tuple. sum(t) 9
sorted(tuple) Returns a list with the elements sorted; the example converts that list to a tuple. tuple(sorted(t)) (1, 1, 2, 2, 3)
reversed(tuple) Returns an iterator over the elements in reverse order; the example collects those elements in a tuple. tuple(reversed(t)) (2, 1, 3, 2, 1)

An iterator supplies values one at a time when a loop or constructor requests them. None of these operations changes t. t.index(value) raises ValueError when the value is absent, and max() and min() require at least one element. For the numeric tuple in the table, sum() adds all values.

Named tuples

Named tuples give names to positions. The namedtuple function from the collections module creates a tuple type with named fields; technically, that type is a subclass of tuple. In Listing 6, Point names a type whose two fields are x and y. Point(1, 2) creates a point, and p.x reads its first field by name.

Listing 6: Creating and using a named tuple in Python.
from collections import namedtuple

# Define a named tuple called 'Point' with fields 'x' and 'y'
Point = namedtuple("Point", ["x", "y"])

# Create a new Point object
p = Point(1, 2)

# Access the fields by name
print(p.x)  # Output: 1
print(p.y)  # Output: 2
1
2

The output is 1 and 2 on separate lines. Named fields make the intended meaning visible without remembering an index. The tuple's fields remain fixed after creation, as with ordinary tuples.

Zip function

The zip function pairs values by position. It accepts iterable objects, which are objects a loop can visit, such as lists, tuples, and strings. It returns an iterator of tuples. The first tuple contains the first value from each input, the second contains the second value, and so on.

Combining parallel sequences

Two parallel sequences describe corresponding information in the same order. Listing 7 pairs each city with its arrival time. Wrapping zip(...) in list(...) collects the pairs so you can print and inspect them. The same listing also creates consecutive city pairs using slices.

Listing 7: Combining two lists into a list of tuples using the zip function.
route = ["City A", "City B", "City C"]
arrival_time = ["10:00", "11:30", "13:00"]

# Combine the two lists into a list of tuples
route_times = list(zip(route, arrival_time))

# Origin and destination pairs
od_pairs = list(zip(route[:-1], route[1:]))

print(route_times)
print(od_pairs)
[('City A', '10:00'), ('City B', '11:30'), ('City C', '13:00')]
[('City A', 'City B'), ('City B', 'City C')]

The first printed list is [('City A', '10:00'), ('City B', '11:30'), ('City C', '13:00')]. Pairing follows position, so the inputs must use the same city order.

You can also visit the generated pairs directly in a loop. Listing 8 unpacks each fruit and price into separate variables.

Listing 8: Iterating over two lists simultaneously using the zip function.
fruits = ["apple", "banana", "cherry"]
prices = [1.00, 0.50, 2.00]

for fruit, price in zip(fruits, prices):
    print(f"{fruit}: ${price}")
apple: $1.0
banana: $0.5
cherry: $2.0

The output is apple: $1.0, banana: $0.5, and cherry: $2.0 on separate lines.

Adjacent pairs

In Listing 7, route[:-1] contains all cities except the last, and route[1:] contains all cities except the first. Pairing the slices gives [('City A', 'City B'), ('City B', 'City C')]. Each pair describes one consecutive journey segment. The two slices have the same length.

Unequal input lengths

By default, zip stops when its shortest input ends. Remaining elements in a longer input do not appear in the result. Listing 9 shows one fruit with no corresponding price. See the Python zip documentation for the stopping rule and the strict=True option.

Listing 9: Observe zip's stopping rule when three fruits have only two corresponding prices.
fruits = ["apple", "banana", "cherry"]
prices = [1.00, 0.50]
print(list(zip(fruits, prices)))

try:
    pairs = list(zip(fruits, prices, strict=True))
except ValueError as error:
    print("ValueError:", error)

The first output is [('apple', 1.0), ('banana', 0.5)]. With strict=True, the calculation raises ValueError when the lengths differ. Use that option when every input item must have a corresponding partner. The error occurs when the iterator is consumed, here by list(...).

Separating paired values

The zip function can also separate a list of pairs into sequences. Listing 10 uses *pairs to supply each pair as a separate argument to zip. For these inputs, zip generates one tuple of fruit names and one tuple of prices. Unpacking assigns those tuples to fruits and prices.

Listing 10: Separate fruit-price pairs into a tuple of fruit names and a tuple of prices.
pairs = [
    ("apple", 1.00),
    ("banana", 0.50),
    ("cherry", 2.00)]

# Separate the paired values into tuples
fruits, prices = zip(*pairs)

print(fruits)
print(prices)
('apple', 'banana', 'cherry')
(1.0, 0.5, 2.0)

The output is ('apple', 'banana', 'cherry') followed by (1.0, 0.5, 2.0). These returned sequences are tuples. If you need lists that can be changed, convert them with list(fruits) and list(prices). This example assumes at least one pair, with exactly two values in every pair.

6.1.6 Exercises

6.1.Q1 Trace tuple unpacking

The program in Listing 11 processes three pairs of integers. In each iteration, unpacking assigns the pair's first value to first and its second value to second. The program adds first to a running total and calculates the current pair's difference.

Listing 11: Unpack three integer pairs and update a running total and the current difference.
pairs = [(2, 5), (4, 1), (0, 3)]
running_total = 0
for first, second in pairs:
    running_total += first
    difference = first - second
    # Record the state here.
print(running_total, difference)

Complete Table 3 at the marked checkpoint after each iteration. The first row shows how to record the values. State the final printed output and explain whether unpacking changes the tuples in pairs.

Table 3: State to record after updating the total and difference for each integer pair.
Iteration Current pair first second running_total difference
1 (2, 5) 2 5 2 -3
2 (4, 1)
3 (0, 3)

The following optional exercise applies the pairs produced by zip to numeric sequences. Review the zip function before attempting it.

6.1.Q2 Element-wise multiplication

Create a function that takes two vectors of the same length as input and returns a new vector containing the element-wise multiplication of the input vectors.

A vector here is a list of numbers. Element-wise multiplication multiplies the values at matching positions. For example, [2, 3] and [5, 7] produce [10, 21]. Leave both input lists unchanged, and return a list in the original position order. Two empty input lists produce an empty list.

Present your algorithm in Python, pseudocode, or a flowchart. If you use Python, you may use zip to visit matching values as pairs. The starter and supplied checks are in Listing 12.

Listing 12: Starter function and supplied checks for multiplying values at matching vector positions.
def elementwise_multiply(
    vector1: list[float], vector2: list[float]
) -> list[float]:
    # Add code here!
    pass


def test_elementwise_multiply() -> None:
    # Input list 1
    vector1 = [1, 2, 3]

    # Input list 2
    vector2 = [4, 5, 6]

    result = elementwise_multiply(vector1, vector2)
    assert result[0] == 4
    assert result[1] == 10
    assert result[2] == 18


test_elementwise_multiply()

For optional practice with nested records, use unpacking and a nested search to keep the best grade paired with the student who received it.

6.1.Q3 Best student finder

Table 4 contains student IDs and scores for Programming, Probability, and Calculus. The final two rows illustrate the information that the function must determine for every subject.

Table 4: Matrix of student scores in different subjects.
Student ID / output Programming Probability Calculus
s01 85 75 90
s02 70 80 85
s03 75 70 80
Best grade
Best student

To analyze student performance and help select next year's teaching assistants, develop a function named find_best_students_per_subject. It must take a list in which each item contains a student ID and a list of subject scores. Use while loops to determine the best grade and student for each subject, and return one (best_grade, student_id) tuple per subject, in the original subject order. If students tie, return the first one in the input list.

Assume the input contains at least one student and at least one subject. Every student has the same number of integer scores, with subject positions in the same order. Leave the input and its score lists unchanged.

Present your algorithm in Python, pseudocode, or a flowchart using while loops. The Python starter in Listing 13 preserves the supplied scores and checks the required result. Add a check for a tie.

For example, in Table 4, the best Programming grade is 85 and the student who obtained it is s01. Therefore, the first returned tuple must be (85, "s01").

Listing 13: Starter function and supplied checks for finding the best student in each subject.
def find_best_students_per_subject(
    student_scores: list[tuple[str, list[int]]],
) -> list[tuple[int, str]]:
    # Add code here!
    pass


scores = [
    ("s01", [85, 75, 90]),
    ("s02", [70, 80, 85]),
    ("s03", [75, 70, 80]),
]

assert find_best_students_per_subject(scores) == [
    (85, "s01"),
    (80, "s02"),
    (90, "s01"),
]