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
# one student: a name, a class, and a tuple of three marks
student = ('Riya', 12, (78, 85, 91))
print(student)
print(len(student))('Riya', 12, (78, 85, 91))
3len(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:
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 markRiya (78, 85, 91) 85
student→('Riya', 12, (78, 85, 91))the whole thingstudent[2]→(78, 85, 91)item number 2 — a tuplestudent[2][1]→85item 1 of that tupleThere 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.
student = ('Riya', 12, (78, 85, 91))
print(type(student[0]))
print(type(student[2]))<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.
# four rows, two columns each
records = (('Riya', 78), ('Amit', 85), ('Sara', 62), ('John', 91))
for r in records:
print(r[0], 'scored', r[1])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:
# 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)Riya scored 78 Amit scored 85 Sara scored 62 John scored 91
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.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.
# 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)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 = nameBoth 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.
# 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))Riya - total 254 - average 84.67 Amit - total 218 - average 72.67
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.
7Recap
And it counts as one item. len(('Riya', 12, (78, 85, 91))) is 3.
student[2][1] is student[2] — a tuple — followed by [1]. No new rule, just the old one twice.
One row per record, the same columns in every row. This is the shape almost all real data arrives in.
for name, marks in records: beats for r in records: because the body says marks, not r[1].
- 1
From a tuple of
(city, temperature)rows, print only the cities above 40.Hint ·
for city, temp in readings:and oneif temp > 40:. - 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 = 0collector above the loop. - 3
Find the cheapest item in a tuple of price records.
Hint · The champion program with
<instead of>, starting at row zero. - 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
Falseand turnsTrueon the first low mark.
What does len(('Riya', 12, (78, 85, 91))) print?
records = (('Riya', 78), ('Amit', 85)). What is records[1][0]?
Why write for name, marks in records: instead of for r in records:?