Matheion

MA-PY1 · Module 2.4

Tuples and grouping related data

Topics: Tuples · Unpacking · Nested data

Overview

Module 2.4 — Tuples and grouping related data

Scroll Core ideas → worked snippets → Practice → Quiz. This is a bridge module: it ties together the lists, strings, and dictionaries you already know, and quietly sets up why classes exist in the next unit. Use any Python REPL, script, or notebook you like.

Concepts

Core ideas — grouping related values

So far you have two main containers. A list is a growable, ordered, changeable sequence — good when you have "many of the same kind of thing" and the number can change. A dict maps keys to values — good for "look this up by name." This module adds one more shape and, more importantly, teaches you to think about the shape of your data before you start looping over it. That habit is what makes the jump to classes (next unit) feel natural rather than abrupt.

Tuples: small, fixed, unchangeable records

A tuple is an ordered group of values written with commas, usually inside parentheses:

point = (3, 4)
waypoint = ("Ridge", 1820, 12.4)   # name, elevation, distance

Three things make tuples distinct from lists:

  1. They are fixed-size. A tuple of three items always has three items. That is a feature: it signals "this is one record with a known set of fields," not "a pile of items that grows."
  2. They are immutable. You cannot reassign waypoint[1] = 1900; it raises a TypeError, exactly like trying to edit a character of a string. If something should never be edited in place, a tuple enforces that.
  3. Because they are immutable, they are hashable. Recall from the previous module that dict keys and set members must be hashable. A tuple of immutable values can therefore be a dict key — a list cannot.

You will most often create a tuple just by writing values separated by commas (the parentheses are optional but usually clearer), and you will most often take one apart with unpacking, described next.

Unpacking: pulling a record into named pieces

When a tuple represents a record, you rarely want waypoint[0], waypoint[1], waypoint[2] scattered through your code — those numbers say nothing about meaning. Unpacking assigns the pieces to readable names in one line:

name, elevation, distance = waypoint

Now name is "Ridge", elevation is 1820, and distance is 12.4. This is the same mechanism that lets a function return several values at once (you saw q, r = divmod_like(17, 5) earlier) and the same mechanism that powers for name, score in pairs: loops. One idea, reused everywhere.

If you only need some pieces, the throwaway name _ signals "I am deliberately ignoring this":

name, _, distance = waypoint    # elevation not needed here

Tuple vs list: a quick decision rule

Ask: would it ever make sense to add or remove an element here?

  • Yes → use a list (a shopping cart, a log that grows, search results).
  • No, this is one fixed record with meaningful positions → use a tuple (a coordinate pair, a row of (name, score), an RGB colour).

This is a soft guideline, not a law, but following it makes your intent obvious to the next reader (often future-you).

Nesting: building richer shapes from simple parts

Real data is rarely flat. You combine the containers you know:

  • A list of tuples is a table of records: [("Ada", 91), ("Lin", 88)].
  • A dict of lists groups many items under each key: {"north": ["Ridge", "Saddle"], "south": ["Creek"]}.
  • A dict of dicts models "look up an entity, then its fields": {"Ada": {"score": 91, "team": "north"}}.

There is no new syntax to memorise — you just put one container inside another and index step by step (table[0][1], groups["north"][0]). The skill is choosing the nesting that matches the question you will ask most often.

From records toward objects (the bridge)

Notice a tension. A tuple ("Ridge", 1820, 12.4) is compact, but the positions are silent — you must remember that index 1 is elevation. A dict {"name": "Ridge", "elevation": 1820, "distance": 12.4} labels each field, but you must repeat those key strings everywhere, and nothing stops a typo like "elevaton".

When the same set of fields shows up again and again — and especially when you start writing functions that always take "a waypoint" as their argument — you are really describing a new kind of thing. The next unit introduces classes, which let you give that kind of thing a name, fix its fields once, and attach the functions that operate on it. For now, just notice when you are passing the same bundle of values around: that is the signal that a class is coming.

Self-study checklist

  • State, in one sentence each, when you would reach for a tuple versus a list.
  • Take a (name, score) pair and unpack it into two names, then ignore the score with _.
  • Explain why a tuple can be a dict key but a list cannot.
  • Spot a place in your own code where you keep passing the same 2–3 values together — that is a future class.

Examples

Worked snippets — records, unpacking, and nesting

A tuple is one record

waypoint = ("Ridge", 1820, 12.4)   # name, elevation (m), distance (km)
print(waypoint[0])   # Ridge
print(len(waypoint)) # 3

The tuple bundles three related facts into a single value you can pass around as a unit. Trying to edit it fails on purpose:

waypoint[1] = 1900
# TypeError: 'tuple' object does not support item assignment

That error is a guarantee: once built, this record will not silently change underneath you.

Unpacking makes the fields readable

name, elevation, distance = waypoint
print(f"{name} sits at {elevation} m, {distance} km in")
# Ridge sits at 1820 m, 12.4 km in

Compare that to writing waypoint[1] and waypoint[2] — the unpacked version reads like a sentence. When you only care about some fields, ignore the rest with _:

name, _, distance = waypoint
print(name, distance)   # Ridge 12.4

A list of tuples is a small table

This is the everyday shape for "several records of the same kind." Loop over it with unpacking directly in the for line:

trail = [
    ("Trailhead", 1400, 0.0),
    ("Ridge", 1820, 12.4),
    ("Summit", 2100, 18.7),
]

for name, elevation, distance in trail:
    print(f"{name:10} {elevation} m  @ {distance} km")

Output:

Trailhead  1400 m  @ 0.0 km
Ridge      1820 m  @ 12.4 km
Summit     2100 m  @ 18.7 km

We can answer questions by walking the table once. For example, find the highest point with a running "best so far" — the accumulator pattern from the loops module, now carrying a whole record:

highest = trail[0]
for stop in trail:
    if stop[1] > highest[1]:    # compare elevation fields
        highest = stop
print("Highest:", highest[0], highest[1])   # Highest: Summit 2100

A dict of lists groups records under a key

When records belong to categories, a dict whose values are lists keeps each group together:

by_region = {
    "north": ["Ridge", "Saddle"],
    "south": ["Creek"],
}

by_region.setdefault("north", []).append("Notch")
print(by_region["north"])   # ['Ridge', 'Saddle', 'Notch']

setdefault(key, []) says "give me the list under this key, creating an empty one first if needed," which avoids a KeyError the first time a region appears.

A tuple as a dictionary key

Because tuples are hashable, a coordinate pair can index a dict — handy for a sparse grid where most cells are empty:

heights = {}
heights[(0, 0)] = 1400
heights[(3, 1)] = 1820
print(heights[(3, 1)])   # 1820

A list could not do this: heights[[3, 1]] = ... raises TypeError: unhashable type: 'list'.

The same bundle, three ways (why classes are coming)

Look at one waypoint expressed three ways and feel the trade-off:

as_tuple = ("Ridge", 1820, 12.4)              # compact, but positions are silent
as_dict  = {"name": "Ridge", "elevation": 1820, "distance": 12.4}  # labelled, but verbose & typo-prone

# Next unit: a class fixes the field names once and travels with its own behaviour.
# class Waypoint:
#     def __init__(self, name, elevation, distance): ...

If you find yourself writing as_dict for the hundredth waypoint, or passing (name, elevation, distance) into every function, that repetition is the cue to define a class — which is exactly where Unit 3 begins.

Practice check — tuples and grouping data

Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.

  1. 1.What happens when you run `wp = ('Ridge', 1820); wp[1] = 1900`?

  2. 2.After `name, _, distance = ('Ridge', 1820, 12.4)`, what is `distance`?

  3. 3.You are collecting practice scores that keep arriving, each as a `(student, score)` record. Which shape best fits?

  4. 4.Which value can be used directly as a dictionary key?

  5. 5.What does this leave in `groups`? ```python groups = {} for name, team in [('Ada', 'n'), ('Lin', 'n'), ('Mo', 's')]: groups.setdefault(team, []).append(name) ```

  6. 6.You keep passing the same `(name, elevation, distance)` trio into every function. What does that repetition usually signal?