enumerate() and zip() in Python: Loop Over Multiple Sequences Cleanly
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)}
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.
* unpacking operator: keys, values = zip(*pairs). This transposes rows into columns, giving you back two separate tuples.