zip and enumerate are two of Python’s most useful built-ins. Once you know them, you’ll spot uses everywhere.

zip — walk two (or more) iterables together

zip(a, b) produces tuples pairing items from each:

names: list[str] = ["alice", "bob", "carol"]
ages: list[int] = [30, 25, 35]

for name, age in zip(names, ages):
    print(f"{name} is {age}")
alice is 30
bob is 25
carol is 35

This replaces the un-Pythonic pattern of looping by index:

# avoid
for i in range(len(names)):
    print(f"{names[i]} is {ages[i]}")

zip is shorter, faster, and reads naturally.

zip with more than two iterables

You can zip any number of iterables together:

names: list[str] = ["alice", "bob", "carol"]
ages: list[int] = [30, 25, 35]
cities: list[str] = ["chennai", "mumbai", "pune"]

for name, age, city in zip(names, ages, cities):
    print(f"{name}, {age}, {city}")

zip stops at the shortest

If the iterables are different lengths, zip stops when the shortest runs out:

list(zip([1, 2, 3, 4], ["a", "b"]))
# [(1, 'a'), (2, 'b')]

If you instead want it to keep going (filling missing values), use itertools.zip_longest:

from itertools import zip_longest

list(zip_longest([1, 2, 3, 4], ["a", "b"], fillvalue="-"))
# [(1, 'a'), (2, 'b'), (3, '-'), (4, '-')]

Building dictionaries with zip

A clean one-liner:

keys: list[str] = ["host", "port", "scheme"]
values: list[str] = ["localhost", "8080", "https"]

config: dict[str, str] = dict(zip(keys, values))
print(config)
# {'host': 'localhost', 'port': '8080', 'scheme': 'https'}

Unzipping with zip

The same zip can also reverse the operation, using the * unpacking we saw in Section 5:

pairs: list[tuple[str, int]] = [("alice", 30), ("bob", 25), ("carol", 35)]
names, ages = zip(*pairs)
print(names)   # ('alice', 'bob', 'carol')
print(ages)    # (30, 25, 35)

This pattern often shows up when splitting batches of (input, target) pairs.

enumerate — get an index along with each value

When you’re looping over a list and need the position too:

fruits: list[str] = ["apple", "banana", "cherry"]

for index, fruit in enumerate(fruits):
    print(f"{index}: {fruit}")
0: apple
1: banana
2: cherry

Without enumerate, the only options were ugly:

# avoid
for i in range(len(fruits)):
    print(f"{i}: {fruits[i]}")

Starting at a different number

By default enumerate starts at 0. Pass a start to change it:

for line_no, line in enumerate(open("file.txt"), start=1):
    print(f"{line_no}: {line.rstrip()}")

Useful when you want human-friendly numbering (line 1, line 2, …).

When to use each

zip       — multiple iterables, walk them in parallel
enumerate — one iterable, want the position too

If you find yourself writing range(len(x)), the right tool is almost always enumerate or zip.

A real-world example

Pair predictions with their true labels and print the wrong ones:

predictions: list[str] = ["cat", "dog", "cat", "bird", "dog"]
labels: list[str] = ["cat", "cat", "cat", "bird", "fish"]

for i, (pred, label) in enumerate(zip(predictions, labels)):
    if pred != label:
        print(f"item {i}: predicted {pred}, actual {label}")
item 1: predicted dog, actual cat
item 4: predicted dog, actual fish

enumerate(zip(...)) is one of the most common combinations in ML training scripts.

What’s next

You can pair iterables and number their items. Next, sorted() — sorting in detail.

Toggle theme (T)