NOETRION

Choose a topic

DAY 09 OF 30 · FUNCTIONS AND COLLECTIONS

Tuples and unpacking

Represent fixed-position records and return several results with deliberate unpacking.

90–120 minutes suggested study timePython 3.12Not yet completed

Today's outcomes

  • Create single-item and multi-item tuples.
  • Use indexing, slicing, and unpacking.
  • Explain immutability without assuming nested objects are immutable.

Before you start: Complete Day 08 and its practical challenge.

Understand the concept

A tuple fixes its sequence of references

A tuple is an ordered sequence whose item references cannot be replaced, added, or removed after creation. It supports indexing and slicing like other sequences. Use it for a small fixed-position result when the positions have clear meaning, such as a count, total, and mean returned together.

The comma creates a tuple in ordinary tuple expressions. Parentheses often improve readability, but (7) is just the number 7, whereas (7,) is a one-item tuple. An empty tuple is (). This small syntax distinction matters when a function expects a sequence and receives a number instead.

Immutability is not a guarantee that nested objects cannot change. A tuple containing a list keeps its reference to that list, while the list itself can still be appended to. If your contract requires deeply immutable data, every relevant contained object must meet that requirement too.

Unpacking assigns positions to names

count, total, mean = result assigns the three items to three names. Ordinary unpacking requires the number of values to match the number of targets. Too many or too few values raises ValueError. Starred unpacking can collect a variable remainder into a list, but use it only when a variable-width record is actually intended.

Unpacking improves readability because later calculations can use meaningful names instead of repeated numeric indexes. It also creates a dependency on record shape. If a function later returns four values, an existing three-target caller fails. Treat the returned shape as part of the interface.

Positional records need a documented order

A tuple such as (3, 90, 30.0) has clear meaning only if the reader knows the positions. Write that order in the function's docstring and examples. A dictionary, introduced tomorrow, may be clearer when records have many named fields or optional fields.

Do not choose a tuple solely because it seems more “professional.” Choose it because fixed order and nonreplacement are useful for the task. Lists remain appropriate for a collection of records that grows over time; dictionaries remain appropriate for named lookups.

Worked example

def session_stats(values):
    recorded = []
    for value in values:
        if value is not None:
            recorded.append(value)
    count = len(recorded)
    total = sum(recorded)
    mean = None if count == 0 else total / count
    return count, total, mean

result = session_stats([30, None, 60, 0])
count, total, mean = result
print("Count:", count, "Total:", total, "Mean:", mean)
print("First two fields:", result[:2])
print("Single-item tuple:", (count,))
assert result == (3, 90, 30.0)
assert session_stats([]) == (0, 0, None)

The ordinary loop keeps every measured value, including zero, and excludes only None. This separates an absent measurement from a valid zero without introducing shorthand before Day 14.

Position Assigned name Meaning Example
0 count Number of recorded observations 3
1 total Sum of recorded minutes 90
2 mean Average, or None for no data 30.0

Changing the order to (total, count, mean) would keep the tuple length but silently alter the caller's interpretation. A shape test alone cannot detect that semantic swap; tests must check the actual field values.

Here is a separate nested-state demonstration:

record = ("Alex", [30, 60])
record[1].append(45)
assert record == ("Alex", [30, 60, 45])
print(record)

The tuple still contains the same name reference and list reference. The referenced list changed. Replacing record[1] directly would be a different operation and is not allowed by tuple item assignment.

Guided practice

  1. Predict: compare type((7)) with type((7,)). Explain the role of the comma without calling parentheses “the thing that makes a tuple.”
  2. Modify: return a minimum and maximum pair for a known nonempty list. Choose and document a separate result for an empty list instead of calling min on an empty sequence.
  3. Reconstruct: use a tuple to return the updated queue and removed task from yesterday's function. Unpack both into descriptive names and verify the source list is unchanged.

For practice with starred unpacking, try first, *middle, last = (1, 2, 3, 4). Expect first 1, middle [2, 3], and last 4. Explain why middle is a list even though the source is a tuple.

Daily challenge

Implement bounds(values) for a list of integers. Return (minimum, maximum) for a nonempty list and (None, None) for an empty list. Leave the input unchanged. The caller must unpack the result and print labeled values.

Values Expected result
[30, 60, 45] (30, 60)
[0] (0, 0)
[-5, 10] (-5, 10)
[] (None, None)

Negative numbers are allowed here because this is a general integer-bounds function, not a validated duration function. The domain differs from yesterday's session contract. Reusing a data structure does not imply reusing every earlier validation rule.

Reference solution

Compare after attempting the challenge
def bounds(values):
    """Return minimum and maximum, or (None, None) for no values."""
    if not values:
        return None, None
    return min(values), max(values)

source = [30, 60, 45]
snapshot = source.copy()
low, high = bounds(source)
assert (low, high) == (30, 60)
assert bounds([0]) == (0, 0)
assert bounds([-5, 10]) == (-5, 10)
assert bounds([]) == (None, None)
assert source == snapshot
print("Minimum:", low, "Maximum:", high)

The empty guard avoids asking min or max to find an observation that does not exist. None explicitly identifies the lack of a bound. Using zero instead would confuse an empty list with an observed zero.

Debug and explain

Mistake Consequence Check
Omit comma in a single-item tuple Ordinary value returned Inspect type and length expectation
Swap positions in a return value Caller labels values incorrectly Check exact field values
Assume a contained list is frozen Nested state changes unexpectedly Test the inner object separately
Unpack three values into two names ValueError Compare the interface with the caller

Explain whether your challenge result is mutable, whether every object inside an arbitrary tuple is immutable, and how an empty result differs from (0, 0). Tomorrow, named dictionary fields will remove some dependence on positional conventions.

Review and next step

Save the bounds contract and four acceptance cases. Rebuild the empty guard from memory at the next review. Move on when you can explain both tuple shape and the meaning of each position.

Sources and verification

Original teaching examples checked with Python 3.12.14. Library behavior reviewed against these primary references on October 3, 2026. Day 26's examples also use Beautiful Soup 4.14.3.

DAILY CHECKPOINT

Check your understanding

Five questions, 20 points each. Pass at 85% or above, which requires all five correct. Feedback explains each answer; revise the lesson and retry when needed.

01. What makes (1,) a one-item tuple?

02. Can a tuple's item references be reassigned?

03. Can a list stored inside a tuple still change?

04. What must ordinary unpacking match?

05. When is a tuple a useful choice?

Review your practical work

Check these statements after running your own challenge. This is your self-review; the website does not execute or assess your Python code.

Pass the knowledge check and finish the practical self-review to save completion.