Working with Tuple Data

Knowing that tuples exist is one thing; structuring a program around them is another. In real code, tuples tend to represent either records (a row of data where each position has a distinct meaning) or fixed-length groupings returned from a function (a min/max pair, a split result, a coordinate). This lesson focuses on the patterns that recur once you start working with tuple data in volume.

The single most useful pattern is iteration with unpacking. Given rows = [("ana", 30), ("ben", 25)], writing for name, age in rows: gets you both fields on one line without having to index by position. The same pattern applies to enumerate (which yields (index, item) tuples) and to dict.items() (which yields (key, value)).

Named tuples lift this pattern further by giving each position a name. Person = namedtuple("Person", ["name", "age"]) produces a lightweight class that is still a plain tuple under the hood: you can iterate, unpack and hash it, but you can also access p.name for readability. For type-checked code, typing.NamedTuple is the modern, annotated equivalent.

Finally, returning multiple values as a tuple is the idiomatic Python way to hand back a small bundle of results. divmod(a, b) returns (quotient, remainder); str.partition(sep) returns (head, sep, tail). Inside your own functions, build habits early: return a tuple (or a named tuple for clarity) rather than mutating an argument or building a one-use class.

Unpacking patterns that show up everywhere

Positional unpacking (a, b = pair) breaks when the tuple length doesn't match. Use extended unpacking (head, *tail = items) when you know the minimum length but not the maximum. Use nested unpacking (for i, (name, age) in enumerate(rows):) to pull apart a tuple of tuples in a single step.

The underscore _ is the conventional name for values you unpack but do not need: first, _, third = row. It still binds a real variable, but the convention signals to a reader that the value is intentionally ignored.

Typed records with NamedTuple

A typing.NamedTuple subclass is the shortest way to declare a typed immutable record. Fields become attributes (p.name), and p._replace(age=31) returns a new tuple with one field changed — the immutable-update idiom in miniature.

Because a NamedTuple is still a tuple, everything that works on tuples works on it: unpacking, iteration, use as a dict key, pickling, comparison by value. It is by far the cheapest upgrade you can make when a plain tuple starts to feel opaque.

Tools for structuring data with tuples.

ToolPurpose
collections.namedtuple
factory
Creates a tuple-backed class with named fields.
typing.NamedTuple
base class
Typed, annotated named tuple.
zip(*iters)
built-in
Combines parallel iterables into tuples.
enumerate(it, start=0)
built-in
Yields (index, item) tuples.
divmod(a, b)
built-in
Returns (quotient, remainder) as a tuple.
str.partition(sep)
method
Returns (head, sep, tail) as a tuple.
dataclasses.dataclass(frozen=True)
decorator
Richer alternative when methods are needed.
sorted(rows, key=...)
built-in
Sort records by a field via key=itemgetter.

Working with Tuple Data code example

The script processes a small list of records so every pattern is exercised against the same data.

# Lesson: Working with Tuple Data
from operator import itemgetter
from typing import NamedTuple


class Person(NamedTuple):
    name: str
    age: int
    city: str


people = [
    Person("ana", 30, "oslo"),
    Person("ben", 25, "rome"),
    Person("cai", 40, "oslo"),
]

for name, age, city in people:
    print(f"{name:4s} age={age:2d} city={city}")

by_age = sorted(people, key=itemgetter(1))
youngest, *others, oldest = by_age
print("youngest:", youngest.name)
print("oldest:  ", oldest.name)

older_ana = people[0]._replace(age=31)
print("immutable update:", older_ana)

for i, p in enumerate(people, start=1):
    print(f"  #{i}: {p.name}")

pairs = list(zip(["a", "b", "c"], [1, 2, 3]))
print("zipped:", pairs)

What to notice as you read:

1) A NamedTuple subclass is a tuple with field names and type hints.
2) Iterating with three names (`for name, age, city in ...`) unpacks each row automatically.
3) `_replace(age=31)` returns a new Person; the original tuple is never mutated.
4) `zip` pairs two iterables elementwise and produces a list of tuples when materialized.

Try both snippets to practice packing and swapping values.

# Example A: aggregate with a nested unpack
scores = [("ana", 91), ("ben", 78)]
total = sum(score for _, score in scores)
print("total:", total)

# Example B: divmod for base conversion
hours, minutes = divmod(185, 60)
print(f"{hours}h {minutes}m")

These checks hold on any correct tuple-based record.

from typing import NamedTuple
class P(NamedTuple):
    n: str
    a: int
p = P("x", 1)
assert p.n == "x" and p[0] == "x"
assert p._replace(a=2).a == 2 and p.a == 1
assert divmod(10, 3) == (3, 1)

Running the script prints:

ana  age=30 city=oslo
ben  age=25 city=rome
cai  age=40 city=oslo
youngest: ben
oldest:   cai
immutable update: Person(name='ana', age=31, city='oslo')
  #1: ana
  #2: ben
  #3: cai
zipped: [('a', 1), ('b', 2), ('c', 3)]