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
- Predict: compare
type((7))withtype((7,)). Explain the role of the comma without calling parentheses “the thing that makes a tuple.” - 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.
- 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.
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.
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.