Python Part I · Module 6 · Tuples
Reading a Tuple: Indexing and Slicing
This chapter should feel almost entirely like revision, and that is the point of it. Positions in a tuple are numbered exactly as positions in a string and a list are — you learned that model once, in Module 4, and it has not changed since. What is worth your attention here is the small set of things that are genuinely different: what comes back from a slice, why reading a tuple is completely unrestricted even though changing one is not, and the fact that the position rules you now know apply to every ordered collection Python has. Learning one idea three times would be waste; recognising that it is one idea is the lesson.
Foundation11 min readPythonPart IFundamentalsTuplesIndexing
Part I · Module 6 · Chapter 6.3 · Reading a Tuple: Indexing and Slicing
1. The Rules Have Not Changed
point = (10, 20, 30)
print(point[0], point[-1])
print(point[1:])10 30
(20, 30)Position 0 is the first element, -1 is the last, and [1:] is everything from position 1 onwards. If any of that needs explaining, the chapters that own it are 4.4 (indexing), 4.5 (slicing) and 5.2 (both, applied to lists) — this chapter assumes all three.
Both numbering systems reach the same three elements, exactly as before:
point = (10, 20, 30)
print(point[0], point[1], point[2])
print(point[-3], point[-2], point[-1])10 20 30
10 20 30And the last valid index is still one less than the length:
point = (10, 20, 30)
print(len(point), point[len(point) - 1])3 30That is the whole of the transfer. The rest of this chapter is about the parts that are not revision.
2. An Index Gives the Element
As with lists, indexing a tuple hands back whatever the element is, of whatever type it is:
rec = ("smoke_test", "PASS", 12.4)
print(rec[0], type(rec[0]))
print(rec[2], type(rec[2]))smoke_test <class 'str'>
12.4 <class 'float'>For a tuple this matters more than it did for a list, because a tuple is a record: position 0 is the name and position 2 is the runtime, and those are different types on purpose. rec[2] is a float you can do arithmetic with immediately — no conversion, no unwrapping.
And since an element is an ordinary value, operations on it work as they always have:
rec = ("smoke_test", "PASS", 12.4)
print(rec[0][:5])smokeEach bracket applies to whatever is on its left — Chapter 5.2's rule. rec[0] is a string; [:5] then slices that string.
3. A Slice of a Tuple Is a Tuple
Here is the first genuinely new fact:
point = (10, 20, 30)
s = point[1:]
print(s, type(s))
print(len(s))(20, 30) <class 'tuple'>
2Slicing a string gave a string; slicing a list gave a list; slicing a tuple gives a tuple. The pattern is consistent — a slice returns the same kind of collection you sliced.
Which means the distinction Chapter 5.2 drew holds here too, and lands on something you have just learned:
point = (10, 20, 30)
print(point[0], type(point[0]))
print(point[0:1], type(point[0:1]))10 <class 'int'>
(10,) <class 'tuple'>point[0] is the number 10. point[0:1] is (10,) — and that is exactly the one-element tuple from Chapter 6.2, trailing comma and all, produced by Python rather than typed by you.
4. The Slice Rules, Confirmed
Everything Chapter 4.5 established carries over. Rather than re-derive it, here it is working:
point = (10, 20, 30)
print(point[:2])
print(point[2:])
print(point[:])
print(point[:] == point)(10, 20)
(30,)
(10, 20, 30)
TrueNote point[2:] is (30,) — a one-element tuple again, for the same reason.
Negative boundaries work:
point = (10, 20, 30)
print(point[-2:])
print(point[:-1])(20, 30)
(10, 20)And an out-of-range slice still does not raise:
point = (10, 20, 30)
print(point[0:100])
print(point[10:20])
print(point[2:1])(10, 20, 30)
()
()As much as exists, then an empty tuple rather than an empty list or string. Same forgiving behaviour, type adjusted — and the same warning: an empty result where you expected elements usually means a boundary is wrong, and nothing will tell you.
5. Nesting Behaves Identically
A tuple of tuples reads with two brackets, left to right, exactly as a list of lists did in Chapter 5.7:
table = (("clk", 1), ("data", 32))
print(table[1])
print(table[1][0])
print(table[1][1])('data', 32)
data
32table[1] evaluates first to a tuple — the second record — and the next bracket is applied to that. There is no new rule, and the rule does not care whether the levels are lists, tuples or a mixture.
6. One Idea, Three Types
This is what the chapter is really for. The position model you learned once now applies to every ordered collection Python has given you:
"clk" | ["clk", 1] | ("clk", 1) | |
|---|---|---|---|
x[0] | 'c' — a one-character string | 'clk' — the element | 'clk' — the element |
x[-1] | the last character | the last element | the last element |
x[0:1] | 'c' — a string | ['clk'] — a list | ('clk',) — a tuple |
| Out-of-range index | IndexError | IndexError | IndexError |
| Out-of-range slice | empty, no error | empty, no error | empty, no error |
len() counts | characters | elements | elements |
Only one row differs in substance: what an index hands back, which follows from what the collection is made of. A string is made of characters, so you get a character; a list and a tuple are made of elements, so you get an element.
7. Reading Is Never Restricted
A point worth making explicitly, because learners sometimes expect immutability to get in the way.
Everything in this chapter is reading. Indexing reads a position; slicing reads a run of positions and builds a new tuple from what it found. Neither changes anything, so neither is affected by the fact that a tuple cannot be changed.
point = (10, 20, 30)
print(point[1])
print(point[:2])
print(point)20
(10, 20)
(10, 20, 30)point is exactly as it started. The slice produced a new tuple and left the original alone, which is the only thing it could have done.
So immutability costs you nothing when reading. It costs you something only when you want to write — and Chapter 6.4 is about exactly that, including what you get in exchange.
8. Broken on Purpose
Four failures, all executed. The messages are the familiar ones with tuple in them.
p = (10, 20)
print(p[2])IndexError: tuple index out of rangep = (10, 20)
print(p[-3])IndexError: tuple index out of rangeprint(()[0])IndexError: tuple index out of rangeAn empty tuple has no valid position, not even 0 — the same as an empty list and an empty string.
print((1, 2)[1.0])TypeError: tuple indices must be integers or slices, not floatIndexError means the position does not exist; TypeError means the thing in the brackets was never a position. Chapter 5.2 drew that distinction for lists and it is unchanged — only the type name in the message differs.
9. Predict, Run, Observe
Predict every value and type before reading on. Two of these are the Section 3 distinction.
rec = ("smoke", "PASS", 12.4)
print(rec[1])
print(rec[1:2])
print(rec[-1])
print(rec[:2])
print(type(rec[0]))
print(type(rec[0:1]))
print(len(rec[1:]))The verified output:
PASS
('PASS',)
12.4
('smoke', 'PASS')
<class 'str'>
<class 'tuple'>
2Lines 1 and 2 are the whole of Section 3: rec[1] is the string "PASS"; rec[1:2] is ('PASS',), a tuple holding it. They print differently and they are different types, and if you compared rec[1:2] == "PASS" you would get False.
rec[-1] is the float 12.4. rec[:2] is the first two elements as a tuple. And len(rec[1:]) is 2 — stop - start with the stop omitted, counting the elements of the resulting tuple.
10. Exercises
Exercise 1 — reach the fields. For sig = ("reset_n", 1, "input"), write expressions producing: the name, the width, the last field, the first two fields as a tuple, and the number of fields.
Exercise 2 — value and type. Without running anything, give both the value and the type of each, then verify:
(a) sig[0] (b) sig[0:1] (c) sig[-1] (d) sig[1:] (e) sig[:] (f) sig[0][0]
Exercise 3 — element or tuple. Explain in two sentences why rec[0:1] == "smoke" is False while rec[0] == "smoke" is True, and say which one you would use to compare a field against a known value.
Exercise 4 — one idea, three types. For s = "abc", l = ["a", "b", "c"] and t = ("a", "b", "c"), predict the value and type of s[0:1], l[0:1] and t[0:1]. Say in one sentence what the pattern is.
Exercise 5 — find the off-by-one. This raises. Give the valid index range and two corrected versions:
point = (10, 20, 30)
print(point[3])Exercise 6 — nested. For table = (("clk", 1), ("data", 32), ("addr", 16)), predict table[2][0], table[-1][1] and len(table[0]), then verify. Say what each of the two brackets is reaching into.
Exercise 7 — no copy needed. Explain in two sentences why t[:] is a useful idiom for a list but not for a tuple, referring to what Chapter 5.8 said about copies.
11. Summary
- Position rules are unchanged from strings and lists: zero-based,
-1is last,len(x) - 1is the last valid index, stop excluded in slices. - An index gives back the element, of whatever type it is — which for a record means the field's own type, ready to use.
- A slice of a tuple is a tuple.
point[0]is10;point[0:1]is(10,). - One-element tuples appear as results, not just as syntax — a one-element slice has to produce one.
- Omitted and negative boundaries work as before, and an out-of-range slice gives an empty tuple rather than raising.
t[:]has no copying role for a tuple, because there is nothing to protect against.- Nested tuples read left to right, each bracket applied to the result of the one before.
- Indexing is one idea applied to three types. What differs is only what comes back.
- Reading is never restricted by immutability — nothing here changes anything.
IndexError: tuple index out of rangefor a position that does not exist;TypeError: tuple indices must be integers or slicesfor one that was never a position.
Reading a tuple costs nothing. The next chapter is about what happens when you try to write one.
Where this fits
Part of the Python curriculum.
