053. Iteration Over Sequences
Sequences share a unified iteration protocol
053. Iteration Over Sequences
After each poll, Aryan’s RAM monitor holds a snapshot as a list of dicts. He needs to iterate it in several ways depending on the task:
snapshot = [
{"name": "Chrome", "rss_mb": 512.3},
{"name": "Slack", "rss_mb": 210.1},
{"name": "PyCharm", "rss_mb": 890.7},
]
# Simple iteration — just values
for proc in snapshot:
print(proc["name"], proc["rss_mb"])
# enumerate() — when you need a rank number too
for rank, proc in enumerate(snapshot, start=1):
print(f"#{rank} {proc['name']:20s} {proc['rss_mb']:.1f} MB")
# zip() — compare two snapshots side by side
for before, after in zip(snapshot_t0, snapshot_t1):
delta = after["rss_mb"] - before["rss_mb"]
if delta > 50:
print(f"{before['name']} grew by {delta:.0f} MB")The key insight: for x in collection works identically on lists, tuples, strings, ranges, and any other sequence. Aryan writes helpers that accept “any sequence of process records” without caring which container type it is.
💡 Fun fact: Python’s unified iteration protocol — the same for x in collection syntax working on strings, lists, tuples, files, generators, and custom objects — was a deliberate design choice called “duck typing”: if it walks like a sequence and quacks like a sequence, treat it as one. This design means functions written to work on lists automatically work on tuples, generators, and database cursors without modification.
⚠️ Watch out: zip() silently stops at the shorter sequence with no warning or error. If you zip two lists of different lengths expecting all elements to be paired, the extra elements in the longer list are quietly discarded. Use itertools.zip_longest() when you need to process all elements from both sequences, filling missing values with a default.
🤔 Think about it: enumerate(seq) is equivalent to zip(range(len(seq)), seq). Both give you (index, item) pairs. Why does Python provide enumerate() as a built-in when zip(range(...)) already works — and what does this tell you about how Python balances expressiveness with the principle that “there should be one obvious way to do it”?
Learning objectives
- Iterate over strings, lists, tuples uniformly with for loops
- Use enumerate() to get both index and value
- Understand that len() and indexing work on all sequences
Key concepts
- iteration
- sequences
- enumerate()
- protocol
Try it
Concept detail
Python uses an iteration protocol: any object with iter and next can be iterated. All sequences (list, tuple, str, range) support: for x in seq, len(seq), seq[i], in operator.
enumerate(seq) adds an index: for i, item in enumerate(seq): # (0, first), (1, second), … for i, item in enumerate(seq, start=1): # (1, first), (2, second), …
zip(seq1, seq2) pairs elements, stopping at the shorter: for a, b in zip([1,2,3], [“x”,“y”]): # (1,“x”), (2,“y”) — stops at 2
This uniformity means code that works on lists often works on tuples and strings too. Writing “accepts any sequence” makes helpers more reusable and testable.
Solution
def char_count(text):
count = 0
for char in text:
count += 1
return count
def second_item(seq):
for i, item in enumerate(seq):
if i == 1:
return item
return None
def interleave(seq1, seq2):
result = []
length = min(len(seq1), len(seq2))
for i in range(length):
result.append(seq1[i])
result.append(seq2[i])
return resultTests
def test_char_count():
assert char_count("hello") == 5
def test_char_count_empty():
assert char_count("") == 0
def test_second_item_list():
assert second_item([10, 20, 30]) == 20
def test_second_item_string():
assert second_item("abc") == "b"
def test_second_item_tuple():
assert second_item((1, 2, 3)) == 2
def test_second_item_single():
assert second_item([42]) is None
def test_interleave_basic():
assert interleave([1, 2, 3], ["a", "b", "c"]) == [1, "a", 2, "b", 3, "c"]
def test_interleave_unequal():
assert interleave([1, 2], ["a", "b", "c"]) == [1, "a", 2, "b"]