Home Tutorials Built-ins

Built-ins

enumerate() and zip() in Python: Loop Over Multiple Sequences Cleanly

Pyford Notes July 1, 2026 7 min read
Key points
  • enumerate(iterable, start=0) yields (index, value) pairs — no manual counter needed.
  • zip(*iterables) pairs items by position and stops at the shortest sequence.
  • Use itertools.zip_longest(fill_value=None) when sequences may differ in length.
  • dict(zip(keys, values)) is the idiomatic way to build a mapping from two lists.

Why range(len()) is the wrong tool

A common pattern from languages like C or Java is to iterate over indices and use them to access list elements:

Before — fragile index-based loop:

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

for i in range(len(fruits)):
    print(i, fruits[i])

After — idiomatic Python with enumerate:

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

for i, fruit in enumerate(fruits):
    print(i, fruit)
# 0 apple
# 1 banana
# 2 cherry

The enumerate() version is shorter, avoids the len() call, and does not risk an off-by-one error. It also works with any iterable — not just sequences — so you can enumerate a file's lines, a generator, or a custom object without changes.

enumerate() basics

enumerate() wraps any iterable and yields tuples of (count, item). Destructuring those tuples in the for line gives you two clean names to work with:

menu = ["soup", "salad", "pasta", "dessert"]

for index, item in enumerate(menu):
    print(f"  {index}. {item}")
# 0. soup
# 1. salad
# 2. pasta
# 3. dessert

You can also keep the tuple intact if you prefer: for pair in enumerate(menu): print(pair[0], pair[1]), but destructuring is cleaner and the standard style.

Changing the start index

The optional start argument shifts the counter without any arithmetic on your side:

chapters = ["Introduction", "Basics", "Advanced", "Appendix"]

for num, title in enumerate(chapters, start=1):
    print(f"Chapter {num}: {title}")
# Chapter 1: Introduction
# Chapter 2: Basics
# Chapter 3: Advanced
# Chapter 4: Appendix

You can pass any integer — including negative values or numbers other than 0 or 1 — to align the counter with an existing numbering scheme.

zip() basics

zip() takes two or more iterables and pairs their elements by position. It returns a lazy iterator that yields one tuple per step:

Before — manual index-based pairing:

names  = ["Alice", "Bob", "Carol"]
scores = [88, 95, 72]

for i in range(len(names)):
    print(names[i], scores[i])

After — clean pairing with zip:

names  = ["Alice", "Bob", "Carol"]
scores = [88, 95, 72]

for name, score in zip(names, scores):
    print(f"{name}: {score}")
# Alice: 88
# Bob: 95
# Carol: 72

Zipping three or more iterables

zip() accepts any number of iterables. Each yielded tuple contains one element from each source:

names   = ["Alice", "Bob", "Carol"]
scores  = [88, 95, 72]
grades  = ["B", "A", "C"]

for name, score, grade in zip(names, scores, grades):
    print(f"{name}: {score} ({grade})")
# Alice: 88 (B)
# Bob: 95 (A)
# Carol: 72 (C)

Because zip() is lazy, it only evaluates elements as they are consumed. This makes it memory-efficient for large sequences — no temporary list of pairs is built in advance.

Handling unequal lengths with zip_longest

Standard zip() stops as soon as the shortest iterable is exhausted. If you need to process all elements even when lengths differ, use itertools.zip_longest():

from itertools import zip_longest

names  = ["Alice", "Bob", "Carol", "Dan"]
scores = [88, 95]

for name, score in zip_longest(names, scores, fillvalue=0):
    print(f"{name}: {score}")
# Alice: 88
# Bob: 95
# Carol: 0    (filled)
# Dan: 0      (filled)

The fillvalue parameter accepts any object, including None, a sentinel string, or a numeric default. Choose a value that makes downstream processing safe.

Building a dict from two lists

Combining dict() and zip() is the standard recipe for converting a parallel pair of lists into a mapping:

keys   = ["host", "port", "ssl"]
values = ["localhost", 5432, True]

config = dict(zip(keys, values))
print(config)
# {"host": "localhost", "port": 5432, "ssl": True}

You can also build the same result with a dict comprehension if you need to transform values during construction:

raw_values = ["localhost", "5432", "true"]
config = {k: v for k, v in zip(keys, raw_values)}
FAQ: can I zip and enumerate together? Yes. Wrap the zip() call with enumerate() to get a position counter alongside the paired elements: for i, (a, b) in enumerate(zip(list_a, list_b)). Notice the inner parentheses when destructuring the tuple yielded by zip.
FAQ: how do I unzip a list of pairs? Pass the list with the * unpacking operator: keys, values = zip(*pairs). This transposes rows into columns, giving you back two separate tuples.