๐ฆ Iterating Through Tuples with a For Loop
๐ฆ Iterating Through Tuples with a For Loop
A tuple is a fixed-length sequence, so a for loop walks it exactly the way it walks a list or a string. This lesson covers the four ways to do it, a refresher on range(), the very useful trick of giving a for loop two variables (for i, j in โฆ), and two case studies from class: matching up two lists (Lion โ Cub) and building a tuple of (item, type) pairs.
List, or a small record class โ so the closest everyday loop is the enhanced for (int n : nums), or the classic counting loop for (int i = 0; i < nums.length; i++), which is what range(len(nums)) translates to. JavaScript has no tuple either (an array is the stand-in), and its two-variable loop is for (const [i, v] of arr.entries()) โ array destructuring inside the loop header โ which is exactly what for i, v in enumerate(...) does in Python, without the brackets or the .entries() call. One trap to unlearn: JavaScript's for (x in arr) hands you the indexes, while Python's for x in t hands you the values.
sqlite3 or most MySQL drivers, cursor.fetchall() returns a list of tuples, one per row โ so for id, name, price in rows: (two-or-more-variable unpacking) is the everyday way to turn query results into the dictionaries you send back from an endpoint. The "tuple of pairs" case study later in this lesson has exactly that shape.
๐ Four Ways to Walk a Tuple
All four start from the same tuple. The difference is what you get on each pass: the value, an index, both, or a counter you maintain yourself.
i here, though n or num would read better) holds the actual item each time, not a position. Use it whenever you only care about the values.i is now a position (an integer) and nums[i] fetches the item there. Use this only when you genuinely need the position, for example to compare an item with its neighbour.enumerate() hands back a pair (index, value) on every pass, and the two loop variables unpack it. Note the order: index first, value second. In class the names were i and j; index, value is easier to read.enumerate() does for you, with more places to make a mistake (forgetting the += 1, or starting at the wrong number).nums[index] because the loop variable and the index are two ways of reaching the same item.)nums = (34, 56, 78, 90, 21, 32, 54, 65)
first = []
for i in nums:
first.append(i)
second = []
for i in range(len(nums)):
second.append(nums[i])
third = []
for index, value in enumerate(nums):
third.append(value)
fourth = []
index = 0
for i in nums:
fourth.append(nums[index])
index += 1
print(first == second == third == fourth)
print(first)
# True
# [34, 56, 78, 90, 21, 32, 54, 65]
๐ range(): The Three Forms, Once More
Covered in an earlier lesson, but worth a second look, because range(len(...)) is the bridge between "loop over the values" and "loop over the positions".
| Form | Gives you | Example |
|---|---|---|
range(end) | 0 up to, but not including, end | range(5) โ 0 1 2 3 4 |
range(start, end) | start up to, but not including, end | range(2, 8) โ 2 3 4 5 6 7 |
range(start, end, step) | Counting in jumps of step; a negative step counts down | range(10, 0, -2) โ 10 8 6 4 2 |
range() hands out numbers lazily, so wrap it in list() when you want to look at them.print(list(range(5)))
print(list(range(2, 8)))
print(list(range(0, 10, 3)))
print(list(range(10, 0, -2)))
print(list(range(3, 3)))
print(range(5))
print(len(range(0, 100, 7)))
# [0, 1, 2, 3, 4]
# [2, 3, 4, 5, 6, 7]
# [0, 3, 6, 9]
# [10, 8, 6, 4, 2]
# []
# range(0, 5)
# 15
step is what makes the index-based loop useful: every second item, every item backwards, and so on.nums = (34, 56, 78, 90, 21, 32, 54, 65)
for i in range(0, len(nums), 2):
print("even position", i, "->", nums[i])
for i in range(len(nums) - 1, -1, -1):
print("backwards", i, "->", nums[i])
# even position 0 -> 34
# even position 2 -> 78
# even position 4 -> 21
# even position 6 -> 54
# backwards 7 -> 65
# backwards 6 -> 54
# backwards 5 -> 32
# backwards 4 -> 21
# backwards 3 -> 90
# backwards 2 -> 78
# backwards 1 -> 56
# backwards 0 -> 34
range(len(nums)) stops at len(nums) - 1, which is exactly the last valid index, so it can never run off the end of the tuple. That's why the idiom is range(len(t)) and not range(len(t) - 1). The only time you subtract one is when you're also looking at the next item, as in the exercises.
โ๏ธ Two Loop Variables: for i, j in โฆ
When each item the loop hands out is itself a pair, Python can unpack the pair straight into two names. enumerate() is the best-known source of pairs, but it is not the only one.
pairs = [(1, "one"), (2, "two"), (3, "three")]
for number, word in pairs:
print(number, word)
# 1 one
# 2 two
# 3 three
start=1 changes only the number you are given, not which item comes first.colours = ("red", "green", "blue")
for place, colour in enumerate(colours, start=1):
print(f"{place}. {colour}")
# 1. red
# 2. green
# 3. blue
.items() gives (key, value) pairs; zip() walks two sequences side by side, one pair at a time.ages = {"Ann": 31, "Bo": 27}
for name, age in ages.items():
print(name, age)
first_names = ("Ann", "Bo", "Cy")
scores = (90, 75, 82)
for who, score in zip(first_names, scores):
print(who, score)
# Ann 31
# Bo 27
# Ann 90
# Bo 75
# Cy 82
(index, (a, b)).adults = ("Lion", "Cat", "Dog")
young = ("Cub", "Kitten", "Puppy")
for i, (adult, baby) in enumerate(zip(adults, young), start=1):
print(i, adult, baby)
# 1 Lion Cub
# 2 Cat Kitten
# 3 Dog Puppy
The number of names has to match the number of items in each pair. Python tells you straight away when it doesn't:
for a, b in [(1, 2, 3)]:
print(a, b)
# ValueError: too many values to unpack (expected 2, got 3)
for a, b, c in [(1, 2)]:
print(a, b, c)
# ValueError: not enough values to unpack (expected 3, got 2)
Read the message literally: "too many values to unpack (expected 2, got 3)" means the pair had more items than you gave names for (older Python versions leave out the "got 3" part). Fix it by matching the names to the shape of the data, or by collecting the extras (a, *rest), which a later lesson covers.
๐ฆ Case Study: Lion โ Cub (Two Lists in Lock-Step)
From class: two lists stored inside one list, where the item at position n of the first list matches the item at position n of the second. The task is to print each adult next to its baby.
animals_list[0] is the adults, animals_list[1] is the young. The same i reaches the matching item in each, which is why a position is genuinely needed here.animals_list = [["Lion", "Cat", "Dog"], ["Cub", "Kitten", "Puppy"]]
for i in range(len(animals_list[0])):
print(f"{animals_list[0][i]} -> {animals_list[1][i]}")
# Lion -> Cub
# Cat -> Kitten
# Dog -> Puppy
zip() pairs up the matching items for you, so no index (and no double lookup) is needed. The * in front of animals_list spreads its two inner lists out as two separate arguments.animals_list = [["Lion", "Cat", "Dog"], ["Cub", "Kitten", "Puppy"]]
for adult, baby in zip(*animals_list):
print(f"{adult} -> {baby}")
# Lion -> Cub
# Cat -> Kitten
# Dog -> Puppy
zip() version is what you'd normally write once you do. A thing to know about zip(): it quietly stops at the shortest sequence, so if one list were missing an item you would simply get fewer lines, with no error.
๐งช Case Study: A Tuple of (Item, Type) Pairs
The practice task from class: given a tuple holding a mixture of values, build a new tuple that holds each item together with its data type.
tup = ("hello", 89, True, "", "welcome", 34.97, "", False, "bye", "")
my_list = []
for i in tup:
t = type(i)
my_wee_tuple = (i, t)
my_list.append(my_wee_tuple)
my_tuple = tuple(my_list)
for i in my_tuple:
print(i)
# ('hello', <class 'str'>)
# (89, <class 'int'>)
# (True, <class 'bool'>)
# ('', <class 'str'>)
# ('welcome', <class 'str'>)
# (34.97, <class 'float'>)
# ('', <class 'str'>)
# (False, <class 'bool'>)
# ('bye', <class 'str'>)
# ('', <class 'str'>)
In the class version my_tuple = tuple(my_list) sat inside the first loop, so a brand-new tuple was built on every pass and only the last one was kept. The answer comes out the same, but the work is repeated needlessly. Moving that line out, to after the loop has finished (as above), builds the tuple once. A good rule: if a line doesn't depend on the loop variable, it probably doesn't belong in the loop.
item and kind take the two halves of each pair. kind.__name__ is the short name of a type, such as str, without the <class 'โฆ'> wrapper.tup = ("hello", 89, True, "", "welcome", 34.97, "", False, "bye", "")
my_tuple = tuple((item, type(item)) for item in tup)
for item, kind in my_tuple:
print(f"{item!r:<10} -> {kind.__name__}")
# 'hello' -> str
# 89 -> int
# True -> bool
# '' -> str
# 'welcome' -> str
# 34.97 -> float
# '' -> str
# False -> bool
# 'bye' -> str
# '' -> str
That tuple((item, type(item)) for item in tup) line is a preview of a later lesson (a "generator expression"): it does the whole list-then-convert dance in one line. The longer loop above is the one to learn first.
nums = (34, 56, 78)
nums.append(90)
# AttributeError: 'tuple' object has no attribute 'append'nums = (34, 56, 78)
nums[0] = 1
# TypeError: 'tuple' object does not support item assignment In the tuple above, True and False are of type bool, but in Python bool is a kind of int. That has a visible effect if you ever test types with isinstance:
print(type(True))
print(isinstance(True, int))
print(isinstance(89, bool))
# <class 'bool'>
# True
# False
And the empty string "" is still a str: it is just a string with no characters in it.
๐ Quick Reference โ Iterating Through Tuples
| Task | Code | Notes |
|---|---|---|
| Walk each value | for n in nums: | Most common form; use when you don't need the position |
| Walk each index | for i in range(len(nums)): | nums[i] gets the value at that position |
| Walk index and value together | for i, n in enumerate(nums): | Index first, value second |
| Count from 1 instead of 0 | for i, n in enumerate(nums, start=1): | Changes the number, not the order |
| Every second position | for i in range(0, len(nums), 2): | The third argument to range is the step |
| Backwards | for i in range(len(nums) - 1, -1, -1): | Or simply for n in reversed(nums): |
| Two sequences together | for a, b in zip(first, second): | Stops at the shorter one without any error |
| A list of pairs | for a, b in pairs: | The names must match the length of each pair |
| A dictionary's pairs | for k, v in d.items(): | Keys and values together |
| Type of an item | type(x) or type(x).__name__ | The second form gives just str, int, โฆ |
| Build a tuple from a loop | tuple(my_list) | Collect in a list, convert once, after the loop |
Reaching for
range(len(โฆ)) out of habit: it is the direct translation of the for (int i = 0; i < n; i++) loop, and it always works, but if all you do with i is look up nums[i], then for n in nums: says the same thing with less to go wrong. Keep the index form for when the position itself matters.for x in gives values, not indexes: in JavaScript for (x in arr) loops over the index keys, and for (x of arr) over the values. Python's single for x in t is the of version.Swapping the two names from
enumerate(): it gives (index, value), in that order. for value, index in enumerate(t) runs without complaint, but value is then the number and index the item, so your output will look right until you read the labels. Name them index, value (or something more specific) and the mistake is hard to make.A one-item tuple needs its comma:
(5) is just the number 5 in brackets, so for x in (5): fails with TypeError: 'int' object is not iterable. Write (5,) to get a real one-item tuple.You can't change a tuple while looping over it:
nums[i] = โฆ raises TypeError, because tuples are immutable. Build a new list (or tuple) from the results instead, which is what the case study does.The end of
range() is not included: range(1, 5) gives 1, 2, 3, 4. If you need to reach 5, write range(1, 6).