Learn
03.5 | NESTED COLLECTIONS AND COMPREHENSIONS
01 | A COLLECTION CAN CONTAIN COLLECTIONS
A nested collection contains another collection as an item or value. A list of score rows can represent several players; a list of dictionaries can keep each player’s name beside their scores.
Nesting does not create a new mystery type. The outer object has its usual rules, and the selected inner object has its own rules. Read an access in stages so you know what each bracket selects.
Before coding, define what one outer item represents and what the inner values mean. A structure without that agreement is difficult to interpret consistently.
02 | TWO INDICES SELECT TWO LEVELS
For rows = [[8, 6], [4, 9]], rows[1] selects the second inner list, [4, 9]. rows[1][0] then selects its first value, four.
The first bracket chooses a row by convention; the second chooses a position within that row. Swapping them can retrieve a different value. Python does not impose your row/column meaning automatically.
Inner lists can have different lengths. A valid outer index does not guarantee every inner index is valid. Check each selected level when the data shape may vary.
rows = [[8, 6], [4, 9]] print(rows[1]) print(rows[1][0])
EXPECTED OUTPUT
[4, 9] 4
03 | KEEP NAMED RECORDS TOGETHER
A dictionary can store a name and a list of scores. A list can hold several such records. players[1]["scores"][0] selects the second player, then their score list, then its first score.
Writing temporary names for these stages often improves readability. The long access is not inherently cleverer than clear steps.
This record structure prevents names and scores being separated accidentally when records are processed or displayed.
players = [{"name": "Maya", "scores": [8, 6]},
{"name": "Kai", "scores": [4, 9]}]
player = players[1]
scores = player["scores"]
print(player["name"], scores[0])EXPECTED OUTPUT
Kai 4
04 | TRAVERSE WITH NESTED LOOPS
An outer loop selects one row; an inner loop processes that row’s values. Reset a per-row total inside the outer body before the inner loop. Initialise a grand total before both loops.
For rows [8,6] and [4,9], row totals are fourteen and thirteen, giving grand total twenty-seven. Adding each row total once avoids double-counting.
Different row lengths do not require indexing every row with the same assumed width when direct item iteration fits the task.
rows = [[8, 6], [4, 9]]
grand_total = 0
for row in rows:
row_total = 0
for score in row:
row_total += score
print("Row total:", row_total)
grand_total += row_total
print("Grand total:", grand_total)EXPECTED OUTPUT
Row total: 14 Row total: 13 Grand total: 27
05 | START WITH THE CLEAR APPEND LOOP
To build a list of squares, begin with an empty list, visit numbers and append a calculated value. That ordinary loop exposes the source, transformation and destination.
A list comprehension writes this same construction compactly. [number ** 2 for number in range(1, 5)] means calculate one square for each supplied number and collect the results in a new list.
Read the for clause first to identify where values come from, then the expression to see what each output item becomes. The source range’s stop is still excluded.
squares = []
for number in range(1, 5):
squares.append(number ** 2)
print(squares)
squares = [number ** 2 for number in range(1, 5)]
print(squares)EXPECTED OUTPUT
[1, 4, 9, 16] [1, 4, 9, 16]
06 | FILTER BEFORE INCLUDING AN ITEM
A comprehension can add an if clause to decide which source values qualify. [score for score in scores if score >= 5] keeps scores of five or more.
The expression before for defines the output item; the if after the source defines a filter. Keeping a score is different from producing the Boolean result of its test.
Here [score >= 5 for score in scores] would include a Boolean for every source item, rather than excluding low scores. Identify whether the task needs values, transformed values or condition results.
scores = [2, 5, 8, 4] kept = [score for score in scores if score >= 5] checks = [score >= 5 for score in scores] print(kept) print(checks)
EXPECTED OUTPUT
[5, 8] [False, True, True, False]
07 | CLEAN TEXT WITH A TRANSFORMATION
A comprehension can call a familiar method on each source string. [name.strip() for name in names] returns a new list of cleaned text values.
It does not edit the original list’s item bindings or original immutable strings. If you also want to remove blank cleaned names, state that filter explicitly.
Keep the operation readable. A multi-line loop is often better when each item needs complicated error handling or several separate actions.
names = [" Maya ", " Kai ", " "] cleaned = [name.strip() for name in names] non_blank = [name.strip() for name in names if name.strip()] print(cleaned) print(non_blank)
EXPECTED OUTPUT
['Maya', 'Kai', ''] ['Maya', 'Kai']
08 | SET AND DICTIONARY COMPREHENSIONS
Use braces for a set comprehension such as {name.lower() for name in names}. It keeps unique transformed names, with no positional order guarantee.
A dictionary comprehension includes a key:value expression. {number: number ** 2 for number in range(1, 4)} maps each number to its square.
Duplicate produced keys overwrite earlier values rather than creating two identical keys. These are concise tools, not a requirement to replace every readable loop.
squares_by_number = {number: number ** 2 for number in range(1, 4)}
print(squares_by_number)EXPECTED OUTPUT
{1: 1, 2: 4, 3: 9}09 | SHALLOW COPIES CAN SHARE INNER ROWS
rows.copy() makes a new outer list but still refers to the original inner row lists. Changing copy_rows[0][0] therefore changes a shared inner row visible through rows.
For this simple list-of-numeric-lists shape, [row.copy() for row in rows] creates new outer and row lists. It is not a universal deep-copy strategy for arbitrarily nested objects.
Also avoid grid = [[0] * 3] * 2 when independent mutable rows are required: the outer repetition reuses one row. A comprehension creating [0] * 3 anew for each row keeps these simple rows independent.
rows = [[8, 6], [4, 9]] copy_rows = rows.copy() copy_rows[0][0] = 99 print(rows) grid = [[0] * 3 for _ in range(2)] grid[0][0] = 1 print(grid)
EXPECTED OUTPUT
[[99, 6], [4, 9]] [[1, 0, 0], [0, 0, 0]]
10 | NUMBER ITEMS WITH ENUMERATE
enumerate(values, start=1) supplies pairs of a display number and the next value. Unpack each pair into number, value in the for header.
The start sets the numbering, not the source index used for access. It is helpful when displaying quiz options numbered one onwards while the list itself remains zero-based.
The underscore in a comprehension such as for _ in range(2) is a conventional name indicating that the supplied value is not needed.
options = ["list", "set", "tuple"]
for number, option in enumerate(options, start=1):
print(f"{number}. {option}")EXPECTED OUTPUT
1. list 2. set 3. tuple
11 | COMPARE A LOOP AND COMPREHENSION
The demo uses [2, 5, 8, 4]. Choose a transformation or filter, then inspect the source value, the decision and the result built so far. It demonstrates fixed examples rather than evaluating arbitrary source.
12 | YOUR NESTED DATA CHALLENGE
Create two player records with a name and three scores each. Display each player’s total, then use a comprehension to build a list of qualifying scores from one selected player.
Show each access stage for the second player’s final score. Test repeated scores and a score below the filter threshold.
Extension: compare a shallow outer copy with new copied rows. Explain precisely which object changes, rather than claiming that every copy is either entirely linked or entirely independent.
PUT YOUR SKILLS TO WORK
Use the Section 3 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.
Section 3 challenges Python course navigation