PyDA Course
📖 Lesson 10 Beginner ⏱ 12 min ⚡ +10 XP ColabKagglenbviewerBinderDeepnoteGitHub range · enumerate · zip · iteration

Range, Enumerate & Zip

Generate number sequences, track indices, and combine iterables.

🎯 What you'll learn:
  • Use range() to generate number sequences
  • Use enumerate() to get index + value during iteration
  • Use zip() to iterate over multiple sequences in parallel
  • Write Pythonic loops that avoid manual index tracking

Three tools that supersede manual counting

Loops gave you repetition; this lesson hands you the three helpers that keep the counting out of your hands. Each replaces a habit you were taught to write by hand, and each is the answer to a recurring irritation: generating numbers, needing the position of an item, and pairing two lists. Together they are the difference between a loop that types and a loop that reads.

Range: the arithmetic sequence, lazily

In the last lesson you summed with range(5). It deserves a closer look, it is the classical tool for “do this a known number of times”:

python
for i in range(5):
    print(i)  # 0 1 2 3 4

range has three forms, mirroring the arithmetic progression a,a+d,a+2d,a, a+d, a+2d, \ldots:

python
range(5)        # 0, 1, 2, 3, 4
range(2, 8)     # 2, 3, 4, 5, 6, 7
range(0, 20, 3) # 0, 3, 6, 9, 12, 15, 18

One argument gives 0,1,,n10, 1, \ldots, n-1; two give the half-open interval [start,stop)[\text{start}, \text{stop}); three add the common difference dd. Crucially, range is lazy: it records the parameters and computes each value only as the loop asks for it. Asking for a million steps costs no more memory than asking for five, the sequence is never materialized.

Enumerate: the position, without the counter

Want the position of each item? The freshman instinct is a manual counter:

python
fruits = ["apple", "banana", "cherry"]

i = 0
for fruit in fruits:
    print(f"{i}: {fruit}")
    i += 1

The i += 1 is a temptation to drift out of sync: forget one, and position labels scramble. enumerate produces both halves in one step, the index and the item, so there is nothing to keep in sync:

python
for i, fruit in enumerate(fruits):
    print(f"{i}: {fruit}")

# Respondents number people from 1:
for i, fruit in enumerate(fruits, start=1):
    print(f"{i}: {fruit}")

Where a mathematician writes bi=ai+ib_i = a_i + i to attach position to value, enumerate hands the pair (i,ai)(i, a_i) to the loop body directly.

Zip: alignment by position

Two parallel lists, names and scores, cry out to be read together. zip aligns them element by element:

python
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]

for name, score in zip(names, scores):
    print(f"{name}: {score}")
# Alice: 85
# Bob: 92
# Charlie: 78

The pairing is the Cartesian trick of running along both lists with a single cursor, forming the tuples (n0,s0),(n1,s1),(n_0, s_0), (n_1, s_1), \ldots. When the lists differ in length, the pairing stops at the shorter one, so nothing is ever half-paired. If you need the crooked tail too, itertools.zip_longest fills it:

python
import itertools
for pair in itertools.zip_longest([1, 2], [3, 4, 5], fillvalue=0):
    print(pair)  # (1, 3), (2, 4), (0, 5) — no value is dropped

A worked example: the class ledger

Watch the three tools compose. A teacher holds a list of names and a parallel list of scores, and wants a numbered report:

python
names = ["Dina", "Omar", "Sara"]
scores = [78, 91, 85]

for i, (name, score) in enumerate(zip(names, scores), start=1):
    print(f"#{i} {name}: {score}")
# #1 Dina: 78
# #2 Omar: 91
# #3 Sara: 85

print(f"Top score: {max(scores)}")   # Top score: 91

Read the loop header from the inside out: zip pairs each name with its score; the parentheses (name, score) unpack that pair; enumerate numbers the pairs starting at one; and i receives the number. Four gestures that would have cost you a hand-written counter now read like the sentence they describe, position attaches to value, pair by pair, exactly as bi=ai+ib_i = a_i + i attaches an index to every term.

Common pitfalls

  • range is exclusive at the top. range(5) yields 0,1,2,3,40, 1, 2, 3, 4, five numbers, none equal to 55. Think half-open interval, [0,5)[0, 5).
  • enumerate on a dict. Iterating a dict gives its keys; enumerate would number the keys, not the pairs. Use dict.items() when you want key and value.
  • zip with unequal lengths. Elements past the shorter input vanish silently. Notice the loss, or fill with zip_longest.
  • zip is a one-shot iterator. In Python 3, p = zip(a, b) hands you an iterator, not a list: list(p) consumes it, and a second list(p) is empty. Convert eagerly with list(zip(a, b)) when you’ll revisit the pairs.

🧩 Challenges

🧩 Challenge, think first, then reveal

Use enumerate to print each color in colors = ["red", "green", "blue"] with its position starting at 1.

💡 Answer: for i, color in enumerate(colors, 1): print(f"{i}. {color}"), the start argument renumbers the pairings from one.

🧩 Challenge, think first, then reveal

Given keys = ["a", "b"] and values = [1, 2], use zip to build a dictionary.

💡 Answer: dict(zip(keys, values)){"a": 1, "b": 2}, the aligned pairs become the mapping's entries.

🤔 Socratic Questions

  • Why prefer range over spelling the list [0, 1, 2, 3, 4]? What changes when the list would hold a million numbers?
  • Since zip stops at the shortest input, how would you detect which side was shorter? When does that distinction matter?
  • Can enumerate walk a dict? What exactly do the indices number?

✅ Quick check

1. What is list(range(1, 10, 2))?

2. What does list(zip([1, 2], [3, 4, 5])) return?

rangeenumeratezipiteration