enumerate & zip

Reviewed & published by Brayan K

Two tiny built-in functions fix two of the most common looping headaches: "I need the index and the item" and "I have two lists I want to walk together". enumerate() and zip() make both of these clean, readable, and bug-free.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Once you have them, you'll never write a manual counter or index-juggling loop again.

What You'll Learn in This Lesson

1 enumerate() — Index and Item Together

The clumsy way is to keep a separate counter and bump it yourself. It works, but it's noisy and easy to get wrong:

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

# The manual way — extra variable, easy to forget the +=1
i = 0
for fruit in fruits:
    print(i, fruit)
    i += 1

enumerate() does the counting for you, handing back the index and the item as a pair on every loop:

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

for i, fruit in enumerate(fruits):
    print(i, fruit)

# Output:
# 0 apple
# 1 banana
# 2 cherry

The i, fruit on the left is tuple unpacking — enumerate yields (0, "apple"), (1, "banana"), and you unpack each pair into two variables right in the for statement.

🔢 Starting From 1 (and Other Numbers)

For numbered lists that people read, counting from 0 looks odd. The start argument fixes that without changing your data:

tasks = ["Wake up", "Code", "Coffee", "Sleep"]

for num, task in enumerate(tasks, start=1):
    print(f"{num}. {task}")

# Output:
# 1. Wake up
# 2. Code
# 3. Coffee
# 4. Sleep

# start can be any number
for q_num, q in enumerate(["Name?", "Age?"], start=10):
    print(f"Q{q_num}: {q}")
# Q10: Name?
# Q11: Age?

2 zip() — Walk Two Lists in Parallel

When you have related lists — names and scores, questions and answers — zip() pairs them up so you can loop over both at once:

names = ["Ada", "Ben", "Cleo"]
scores = [88, 72, 95]

# Awkward without zip — indexing both by position
for i in range(len(names)):
    print(names[i], scores[i])

# Clean with zip
for name, score in zip(names, scores):
    print(f"{name}: {score}")

# Output:
# Ada: 88
# Ben: 72
# Cleo: 95

You can zip three or more lists at once — and combine zip with enumerate for index + parallel items:

names = ["Ada", "Ben"]
ages = [36, 41]
cities = ["London", "Paris"]

for name, age, city in zip(names, ages, cities):
    print(f"{name} ({age}) — {city}")
# Ada (36) — London
# Ben (41) — Paris

# enumerate + zip together
for i, (name, score) in enumerate(zip(names, [88, 72]), start=1):
    print(f"{i}. {name} scored {score}")
# 1. Ada scored 88
# 2. Ben scored 72
# Build a quiz results report
questions = ["Capital of France?", "2 + 2?", "Largest planet?"]
answers   = ["Paris", "4", "Jupiter"]
correct   = [True, True, False]

print("=== Quiz Report ===")
score = 0
for num, (q, a, ok) in enumerate(zip(questions, answers, correct), start=1):
    mark = "✓" if ok else "✗"
    print(f"Q{num}. {q}")
    print(f"    Your answer: {a}  [{mark}]")
    if ok:
        score += 1

print(f"\nFinal score: {score}/{len(questions)}")

# ✅ Expected output:
# === Quiz Report ===
# Q1. Capital of France?
#     Your answer: Paris  [✓]
# Q2. 2 + 2?
#     Your answer: 4  [✓]
# Q3. Largest planet?
#     Your answer: Jupiter  [✗]
#
# Final score: 2/3

3 When Lengths Don't Match

If the lists are different lengths, zip() simply stops at the shortest one. The extra items are dropped silently:

names = ["Ada", "Ben", "Cleo", "Dan"]
scores = [88, 72]   # only two scores!

for name, score in zip(names, scores):
    print(name, score)
# Ada 88
# Ben 72
# (Cleo and Dan are skipped — no matching score)

If you need to reach the end of the longest list and fill the gaps instead, use zip_longest from the itertools module:

from itertools import zip_longest

names = ["Ada", "Ben", "Cleo"]
scores = [88, 72]

for name, score in zip_longest(names, scores, fillvalue="—"):
    print(name, score)
# Ada 88
# Ben 72
# Cleo —   (missing score filled with the fillvalue)

🔄 Building Dicts & the Unzip Trick

Two lists → one dictionary

keys = ["name", "age", "city"]
values = ["Ada", 36, "London"]

profile = dict(zip(keys, values))
print(profile)
# {'name': 'Ada', 'age': 36, 'city': 'London'}

Wrapping zip() in dict() is the standard one-liner for turning parallel key and value lists into a dictionary.

Unzip — split pairs back into columns

pairs = [("Ada", 88), ("Ben", 72), ("Cleo", 95)]

# The * spreads the pairs; zip regroups by position
names, scores = zip(*pairs)
print(names)    # ('Ada', 'Ben', 'Cleo')
print(scores)   # (88, 72, 95)

zip(*pairs) is the mirror image of zipping — it takes a list of tuples and separates it back into independent columns.

🧩 Reorder Challenge

These lines should print a numbered roster pairing names with roles. Reorder them so the output starts with 1. Ada - lead.

A)   for i, (name, role) in enumerate(zip(names, roles), start=1):
B)   names = ["Ada", "Ben"]
C)       print(f"{i}. {name} - {role}")
D)   roles = ["lead", "dev"]

B → D → A → C

names = ["Ada", "Ben"]                                       # B
roles = ["lead", "dev"]                                      # D
for i, (name, role) in enumerate(zip(names, roles), start=1): # A
    print(f"{i}. {name} - {role}")                            # C (indented)

# Output:
# 1. Ada - lead
# 2. Ben - dev

Why: both lists (B, D) must exist before zip can pair them. The for header (A) zips and enumerates with start=1, and the indented print (C) is the loop body. The combined unpacking i, (name, role) reads the index plus each pair.

🧠 Quick Recall

for i, c in enumerate("ab"):
    print(i, c)

0 a then 1 b — enumerate works on any iterable, including strings, yielding (index, character) pairs starting at 0.

2. How many lines print?

for x, y in zip([1, 2, 3], [9, 8]):
    print(x, y)

2 lines (1 9 and 2 8) — zip stops at the shorter list, so the 3 is never reached.

3. What is the result?

print(dict(zip(["a", "b"], [1, 2])))

{'a': 1, 'b': 2} — zip pairs the keys with the values and dict() turns those pairs into a dictionary.

📋 enumerate & zip Quick Reference

PatternUse
for i, x in enumerate(xs)Index + item, from 0
enumerate(xs, start=1)Human-friendly numbering
for a, b in zip(xs, ys)Walk two lists together
dict(zip(keys, vals))Two lists → dictionary
a, b = zip(*pairs)Unzip into columns

🎯 Mini Challenge: Medal Table

Pair countries with medal counts and print a numbered, formatted table.

# 🎯 YOUR TURN: build a medal table
countries = ["USA", "China", "Japan", "Australia"]
golds     = [39, 38, 27, 17]

print(f"{'#':<3}{'Country':<12}{'Gold':>5}")
print("-" * 20)
for rank, (country, gold) in enumerate(zip(countries, golds), start=1):
    print(f"{rank:<3}{country:<12}{gold:>5}")

# Bonus: turn the two lists into a lookup dict
table = dict(zip(countries, golds))
print("\nJapan's golds:", table["Japan"])

# ✅ Expected output:
# #  Country      Gold
# --------------------
# 1  USA            39
# 2  China          38
# 3  Japan          27
# 4  Australia      17
#
# Japan's golds: 27

Lesson complete — your loops just leveled up!

You can grab the index with enumerate(), number lists from 1, walk multiple lists together with zip(), handle mismatched lengths, build dicts, and even unzip data. These two functions replace a lot of fragile manual loops.

Practice quiz

What does enumerate(items) yield on each iteration?

  • Just the item
  • Just the index
  • A (index, item) pair
  • A (item, index) pair

Answer: A (index, item) pair. enumerate yields tuples of (index, item), letting you unpack both in the for statement.

How do you make enumerate report numbers beginning at 1?

  • enumerate(items, start=1)
  • enumerate(items, 1) is impossible
  • enumerate(items).start(1)
  • enumerate(1, items)

Answer: enumerate(items, start=1). The start argument sets the first index reported; start=1 is ideal for human-facing numbered lists.

Does the start argument of enumerate skip any items?

  • Yes, it skips the first item
  • Yes, it skips start-1 items
  • It depends on the iterable
  • No, it only changes the reported index; every item is still visited

Answer: No, it only changes the reported index; every item is still visited. start only changes the label; all elements are still iterated over.

When you zip lists of different lengths, zip() stops...

  • at the longest list, padding with None
  • at the shortest list, ignoring extra items
  • and raises a ValueError
  • after the first element

Answer: at the shortest list, ignoring extra items. Plain zip() halts at the shortest iterable and silently drops the leftover items of longer ones.

How many lines does `for x, y in zip([1, 2, 3], [9, 8]): print(x, y)` print?

  • 2
  • 1
  • 3
  • 0

Answer: 2. zip stops at the shorter list (length 2), so it prints '1 9' and '2 8' — 2 lines.

Which tool reaches the end of the LONGEST iterable, filling gaps?

  • zip with strict=True
  • enumerate
  • itertools.zip_longest
  • map

Answer: itertools.zip_longest. itertools.zip_longest continues to the longest iterable and pads missing values with fillvalue (None by default).

What does dict(zip(['a', 'b'], [1, 2])) produce?

  • [('a', 1), ('b', 2)]
  • {'a': 1, 'b': 2}
  • {'a': 'b', 1: 2}
  • ('a', 1, 'b', 2)

Answer: {'a': 1, 'b': 2}. zip pairs keys with values and dict() turns those pairs into {'a': 1, 'b': 2}.

Given pairs = [('Ada', 88), ('Ben', 72)], what does names, scores = zip(*pairs) give for names?

  • ['Ada', 'Ben']
  • ('Ada', 88)
  • ('Ada', 'Ben', 88, 72)
  • ('Ada', 'Ben')

Answer: ('Ada', 'Ben'). zip(*pairs) unzips by position, so names is the tuple ('Ada', 'Ben') and scores is (88, 72).

Can you zip three or more iterables at once?

  • No, zip only takes two
  • Yes, zip(a, b, c) yields 3-tuples
  • Only with itertools
  • Only if they are the same length

Answer: Yes, zip(a, b, c) yields 3-tuples. zip accepts any number of iterables and yields tuples with one element from each.

What does `for i, c in enumerate('ab')` produce?

  • ('a', 0) then ('b', 1)
  • 1 a then 2 b
  • 0 a then 1 b
  • a b

Answer: 0 a then 1 b. enumerate works on any iterable including strings, yielding (0, 'a') and (1, 'b').

Continue this course

Frequently asked questions

What does enumerate() do?

enumerate() lets you loop over a sequence and get both the index and the item at the same time. Instead of for item in items plus a manual counter, you write for i, item in enumerate(items). On each pass i is the position (0, 1, 2, ...) and item is the value, which removes a whole class of off-by-one counter bugs.

How do I make enumerate start counting from 1 instead of 0?

Pass the start argument: enumerate(items, start=1). This is perfect for human-facing numbered lists like '1. Apple, 2. Banana' where starting from 0 would look wrong. The start only changes the index it reports; it doesn't skip any items.

What does zip() do?

zip() walks several iterables in parallel, producing tuples of their matching elements. zip(names, scores) yields ('Ada', 88), ('Ben', 72), and so on. It's the clean way to process related lists together — pairing names with grades, questions with answers, or columns of a table row by row.

What happens if the lists I zip have different lengths?

zip() stops at the shortest one and silently ignores the extra items in the longer iterables. If you need to keep going to the end of the longest one and fill gaps, use itertools.zip_longest, which pads missing values with a fillvalue (None by default).

Can I un-zip data back into separate lists?

Yes — zip is its own inverse with the star operator. If pairs = [(1, 'a'), (2, 'b')], then nums, letters = zip(*pairs) gives nums = (1, 2) and letters = ('a', 'b'). This 'unzip' trick is handy for splitting a list of records back into parallel columns.