LambdaLabTM
Computer Science · Class 11 · Tuples Revisited
TuplesNesting⏱️ 13 min read

Tuples Inside Tuples

An item of a tuple can be anything — including another tuple. That one sentence is what turns a tuple from a row of numbers into a table: a tuple of students, each student a tuple of their name and their marks. Every real dataset you will meet has this shape, and reading one takes exactly two new ideas.

1An item that is itself a tuple

student.py
# one student: a name, a class, and a tuple of three marks

student = ('Riya', 12, (78, 85, 91))

print(student)
print(len(student))
Output
('Riya', 12, (78, 85, 91))
3
Key Takeaway
len(student) is 3, not 5. The tuple has three items — a string, a number, and one tuple. The inner tuple is a single item however many marks it holds. Counting the inner items would be len(student[2]).

2Reaching inside: student[2][1]

student[2] hands you the inner tuple. Once you have a tuple, you can index it — so the two brackets go one after the other:

reaching_in.py
student = ('Riya', 12, (78, 85, 91))

print(student[0])       # the name
print(student[2])       # the whole tuple of marks
print(student[2][1])    # the second mark
Output
Riya
(78, 85, 91)
85
Read it left to right
student('Riya', 12, (78, 85, 91))the whole thing
student[2](78, 85, 91)item number 2 — a tuple
student[2][1]85item 1 of that tuple

There is no special rule to learn here. Each pair of brackets is applied to whatever the thing on its left worked out to, so student[2][1] is just student[2] followed by [1]. If you can read one, you can read three.

types.py
student = ('Riya', 12, (78, 85, 91))

print(type(student[0]))
print(type(student[2]))
Output
<class 'str'>
<class 'tuple'>

3A tuple of records — the useful shape

The shape you will meet again and again is a tuple whose items are all the same kind of small tuple. It is a table: one row per student, the same two columns in every row.

records.py
# four rows, two columns each

records = (('Riya', 78), ('Amit', 85), ('Sara', 62), ('John', 91))

for r in records:
    print(r[0], 'scored', r[1])
Output
Riya scored 78
Amit scored 85
Sara scored 62
John scored 91

Each round, r holds one whole row — the tuple ('Riya', 78) — so r[0] is the name and r[1] is the mark. It works. It is also the version that gets misread six lines later, when nobody remembers whether r[1] was the mark or the class.

4Two names in the loop header

Unpacking already lets you take a tuple apart into several variables at once — name, marks = ('Riya', 78). A for loop will do exactly the same thing, on every round, if you put two names where the loop variable goes:

records_unpack.py
# two names in the header: each row is unpacked as it arrives

records = (('Riya', 78), ('Amit', 85), ('Sara', 62), ('John', 91))

for name, marks in records:
    print(name, 'scored', marks)
Output
Riya scored 78
Amit scored 85
Sara scored 62
John scored 91
Key Takeaway
Identical output, and a much better program. Nothing new is happening — the loop still hands over one row per round, and name, marks = row still does the taking-apart. What changed is that the two pieces now have names, so the body reads marks instead of r[1] and cannot be misread.
Watch Out
The names have to match the row exactly. Two names for a three-part row raises ValueError: too many values to unpack (expected 2), and three names for a two-part row raises ValueError: not enough values to unpack (expected 3, got 2). Both messages name the number Python was given and the number you asked for, which is usually enough to find the row that is the odd one out.

5Program — who scored the highest?

The champion program from the lists chapter, with one change: the champion is now two variables, because the answer has to report a name as well as a mark.

topper.py
# the highest scorer, out of a tuple of records

records = (('Riya', 78), ('Amit', 85), ('Sara', 62), ('John', 91))

top_name = records[0][0]
top_marks = records[0][1]

for name, marks in records:
    if marks > top_marks:
        top_name = name
        top_marks = marks

print('Topper:', top_name, 'with', top_marks)
Output
Topper: John with 91
top_name = records[0][0]

The champion starts as a real row of the table — the first one. Starting top_marks at 0 would be safe here and wrong on a table of temperatures, which is the same trap as before.

if marks > top_marks:

Only the marks are compared. The name is carried along for the ride, which is why it has to be updated in the same if — inside the block, not after it.

top_name = name

Both lines move together. Update one and forget the other and the program reports the right mark against the wrong person, with nothing to warn you.

6Program — a report from a deeper table

Now the second column is itself a tuple of marks. The outer loop walks the students; an inner loop walks that student's marks.

report.py
# each student has a tuple of marks — a loop inside a loop

report = (('Riya', (78, 85, 91)), ('Amit', (66, 72, 80)))

for name, marks in report:
    total = 0

    for m in marks:
        total = total + m

    print(name, '- total', total, '- average', round(total / len(marks), 2))
Output
Riya - total 254 - average 84.67
Amit - total 218 - average 72.67
Watch Out
total = 0 is inside the outer loop. It has to be reset for each student. Put it above the outer loop and Amit gets Riya's marks added to his own — 472 instead of 218 — which is a wrong answer that never crashes and never looks obviously wrong.

round(total / len(marks), 2) keeps the output readable: without it the first line ends in 84.66666666666667, which is the true answer and not a useful one to print.

report.py

7Recap

An item can be a tuple

And it counts as one item. len(('Riya', 12, (78, 85, 91))) is 3.

Brackets stack left to right

student[2][1] is student[2] — a tuple — followed by [1]. No new rule, just the old one twice.

A tuple of equal-sized tuples is a table

One row per record, the same columns in every row. This is the shape almost all real data arrives in.

Two names in the header unpack the row

for name, marks in records: beats for r in records: because the body says marks, not r[1].

✍️ Now write these yourself
  1. 1

    From a tuple of (city, temperature) rows, print only the cities above 40.

    Hint · for city, temp in readings: and one if temp > 40:.

  2. 2

    From a tuple of (item, price, qty) rows, print each line total and the bill at the end.

    Hint · Three names in the header this time, and a total = 0 collector above the loop.

  3. 3

    Find the cheapest item in a tuple of price records.

    Hint · The champion program with < instead of >, starting at row zero.

  4. 4

    From a tuple of students each holding a tuple of marks, print whoever failed any subject (below 33).

    Hint · Loop inside a loop, and a flag per student that starts False and turns True on the first low mark.

Quick Check

What does len(('Riya', 12, (78, 85, 91))) print?

Quick Check

records = (('Riya', 78), ('Amit', 85)). What is records[1][0]?

Quick Check

Why write for name, marks in records: instead of for r in records:?