Problem Solved

Python · enumerate

Home › Courses › Python › Problems › enumerate
🐍 Concept · 3 min read

How to iterate with an index.
enumerate, not range(len()).

The most common Python loop question. You need both the index and the value. In C-style languages, you'd write for (int i = 0; i < n; i++). In Python, you write for i, item in enumerate(items). It's not just cleaner — it's faster, works on any iterable, and avoids the off-by-one bugs that range(len()) creates. This page covers enumerate, zip for parallel iteration, and why range(len()) is an anti-pattern.

Short answer: for i, item in enumerate(items): — yields (index, value) tuples. Start at 1: enumerate(items, start=1). Two lists: zip(list1, list2). Both index and multiple lists: enumerate(zip(list1, list2)). Never use range(len(items)) — it's slower, less readable, and fails on iterables that don't support indexing. enumerate returns a lazy iterator, not a list.

✅ enumerate vs range(len())

The pattern you should always use.

enumerate.py
items = ['apple', 'banana', 'cherry']

# ❌ The anti-pattern — range(len())
for i in range(len(items)):
    print(i, items[i])
# 0 apple
# 1 banana
# 2 cherry

# ✅ The Pythonic way — enumerate
for i, item in enumerate(items):
    print(i, item)
# 0 apple
# 1 banana
# 2 cherry

# Start at 1 instead of 0
for i, item in enumerate(items, start=1):
    print(f'{i}. {item}')
# 1. apple
# 2. banana
# 3. cherry

# Works on any iterable — not just lists
for i, char in enumerate('abc'):
    print(i, char)
# 0 a
# 1 b
# 2 c

# Works on generators (which range(len()) can't do)
squares = (x * x for x in range(5))
for i, sq in enumerate(squares):
    print(i, sq)
# 0 0
# 1 1
# 2 4
# 3 9
# 4 16
Why enumerate is better:

1. No indexing back into the sequence
   range(len()):
     for i in range(len(items)):
         item = items[i]   # extra lookup
   
   enumerate:
     for i, item in enumerate(items):
         # item is already here

2. Works on any iterable
   ❌ range(len()) requires:
      - len() support
      - __getitem__ support
   
   ✅ enumerate requires only iteration
   
   Examples that work with enumerate but not range(len()):
     - Generators
     - Files
     - Sets
     - Dictionaries
     - zip objects
     - map objects

3. Faster
   enumerate is O(1) per item.
   range(len()) creates a range, calls len(),
   then indexes the sequence for each item.
   
   For a 1M-item list:
     enumerate: ~50ms
     range(len()): ~80ms
   
   The difference is real, though not huge.
   The readability difference is bigger.

4. Fewer bugs
   range(len()) tempts you to:
     - Use the wrong index (off-by-one)
     - Forget that modifying the list while
       iterating breaks the indexing
     - Assume the sequence is a list
   
   enumerate gives you the value directly.
   You can't have an off-by-one if you
   never touch the index.

When range(len()) is justified:

The rare case: modifying a list while iterating.
  for i in range(len(items) - 1, -1, -1):
      if should_remove(items[i]):
          del items[i]
  → Iterate backwards so deletion doesn't
    shift unprocessed items.
  → enumerate can't do this.
  
  But even this is usually better with a
  list comprehension:
    items = [x for x in items if not should_remove(x)]

The only truly valid use of range(len()):
  When you need the index AFTER iteration,
  or when you're building a data structure that
  references positions.
  
  Even then, enumerate is usually possible.
  Use range(len()) only when you've tried
  enumerate and it doesn't fit.

Rule of thumb:
  For every range(len(x)) you write,
  ask: could this be enumerate(x)?
  The answer is almost always yes.
  Write the enumerate.

✅ Parallel iteration with zip

Two (or more) sequences at once.

zip.py
names = ['Alice', 'Bob', 'Charlie']
scores = [85, 72, 95]

# zip — iterate two sequences together
for name, score in zip(names, scores):
    print(f'{name}: {score}')
# Alice: 85
# Bob: 72
# Charlie: 95

# enumerate + zip — index and multiple sequences
for i, (name, score) in enumerate(zip(names, scores), start=1):
    print(f'{i}. {name}: {score}')
# 1. Alice: 85
# 2. Bob: 72
# 3. Charlie: 95

# Build a dict from two lists
scores_dict = dict(zip(names, scores))
print(scores_dict)
# {'Alice': 85, 'Bob': 72, 'Charlie': 95}

# zip stops at the shortest
short = [1, 2]
long = ['a', 'b', 'c']
for a, b in zip(short, long):
    print(a, b)
# 1 a
# 2 b
# (3 is dropped)

# zip_longest — keep all, fill missing with a value
from itertools import zip_longest
for a, b in zip_longest(short, long, fillvalue='?'):
    print(a, b)
# 1 a
# 2 b
# ? c

# Three or more sequences
for name, score, grade in zip(names, scores, ['A', 'B', 'A']):
    print(name, score, grade)
zip basics:

  zip(a, b) yields (a[0], b[0]), (a[1], b[1]), ...
  Each iteration produces a tuple.

  Stops at the shortest.
  Length of zip(a, b) = min(len(a), len(b)).

Why zip is better than indexing:

❌ for i in range(min(len(a), len(b))):
      x, y = a[i], b[i]

✅ for x, y in zip(a, b):

Same output, cleaner code, faster, safer.

The dict(zip(...)) pattern:

  names = ['Alice', 'Bob', 'Charlie']
  scores = [85, 72, 95]
  d = dict(zip(names, scores))
  # {'Alice': 85, 'Bob': 72, 'Charlie': 95}

  Common for two parallel lists that logically
  form key-value pairs.
  This is how you'd process CSV columns,
  API responses, or paired data.

The zip(*) trick — unzip:

  pairs = [('Alice', 85), ('Bob', 72)]
  names, scores = zip(*pairs)
  # names = ('Alice', 'Bob')
  # scores = (85, 72)

  zip(*iterable) transposes.
  Useful for converting a list of tuples into
  two separate sequences.

Nested enumerate + zip:

  for i, (name, score) in enumerate(zip(names, scores)):
      ...
  
  The tuple unpacking is:
    i ← enumerate's counter
    (name, score) ← zip's tuple
  
  The parentheses around name, score are required
  when unpacking a nested tuple. Without them,
  Python tries to assign three separate variables,
  which fails.

Combined patterns:

  # Index + two sequences
  for i, (a, b) in enumerate(zip(list1, list2)):
  
  # Index + three sequences
  for i, (a, b, c) in enumerate(zip(list1, list2, list3)):
  
  # Dict from three sequences (rare but valid)
  combined = list(zip(names, scores, grades))
  for name, score, grade in combined:
      ...

  # Unzip a list of tuples
  xs, ys = zip(*pairs)

  # Enumerate a dictionary's items
  for i, (key, value) in enumerate(d.items()):
      ...

The enumerate + items pattern:

  for i, (k, v) in enumerate(scores.items(), start=1):
      print(f'{i}. {k}: {v}')

  Useful for numbered lists of dict entries.
  Matches the pattern of enumerate + zip,
  because d.items() yields tuples too.

⚠️ zip is lazy — it doesn't create a list.
   for a, b in zip(huge_list_a, huge_list_b):
       ...  # streams, doesn't materialize

   To force a list: list(zip(a, b)).
   But usually you don't need to.

⚠️ zip truncates silently.
   If the lists have different lengths,
   the extra items are dropped.
   To catch this:
     if len(a) != len(b): raise ValueError
   Or use zip_longest with fillvalue.

✅ Advanced patterns

The cases beyond simple iteration.

advanced.py
# 1. Enumerate a dict's items
scores = {'Alice': 85, 'Bob': 72}
for i, (name, score) in enumerate(scores.items(), start=1):
    print(f'{i}. {name}: {score}')
# 1. Alice: 85
# 2. Bob: 72

# 2. Enumerate with conditional — skip or break
for i, item in enumerate(items):
    if item == 'banana':
        continue    # skip the print
    if item == 'cherry':
        break       # stop iterating
    print(i, item)
# 0 apple

# 3. Enumerate a range
for i, n in enumerate(range(10, 15)):
    print(i, n)
# 0 10
# 1 11
# 2 12
# 3 13
# 4 14

# 4. Find the index of the first match
items = ['apple', 'banana', 'cherry']
banana_index = next(
    (i for i, item in enumerate(items) if item == 'banana'),
    None
)
print(banana_index)  # 1

# 5. Collect indices and values separately
items = [10, 20, 30]
indices = []
values = []
for i, v in enumerate(items):
    indices.append(i)
    values.append(v)
print(indices)  # [0, 1, 2]
print(values)   # [10, 20, 30]

# Or in one line with zip(*enumerate(...))
indices, values = zip(*enumerate(items))
print(indices)  # (0, 1, 2)
print(values)   # (10, 20, 30)

# 6. Sort by original index
sorted_items = sorted(enumerate(items), key=lambda x: x[0])
print(sorted_items)
# [(0, 10), (1, 20), (2, 30)]
Advanced enumerate patterns:

1. Enumerate + dict items
   for i, (k, v) in enumerate(d.items()):
       # i is counter, (k, v) is the item tuple
   
   The double unpacking (i, (k, v)) is common.

2. Find the first match index
   next((i for i, x in enumerate(items) if cond(x)), None)
   → returns the index of the first match
   → or None if no match
   → useful for "where is X in this list?"
   
   Or use items.index(x) — but that raises ValueError
   if not found, and it compares by equality, not condition.
   
   enumerate + next is the general "find where" idiom.

3. Collect indices and values
   indices, values = zip(*enumerate(items))
   → transposes (0, item0), (1, item1), ... into
     (indices_tuple, values_tuple)
   → the same zip(*) trick as unzipping pairs

4. Enumerate and modify the sequence
   for i, item in enumerate(list(items)):  # copy
       items[i] = f(item)
   
   → iterate a copy, modify the original by index
   → avoids the "modifying while iterating" problem
   
   Better: build a new list
   items = [f(x) for x in items]
   
   The enumerate version is for in-place mutation.
   It's rare — prefer the new list.

5. Enumerate with start offset
   enumerate(items, start=100)
   → counts 100, 101, 102, ...
   → useful for IDs, line numbers, chapters
   
   Often used for 1-based numbering:
   enumerate(items, start=1)

6. Reverse iteration with enumerate
   for i, item in reversed(list(enumerate(items))):
       print(i, item)
   → iterates backwards, index preserved
   → reversed() doesn't work on enumerate objects
     directly (enumerate isn't reversible)
   → so wrap in list() first, then reversed
   → O(n) memory for the list
   → for large sequences, use:
     for i in range(len(items) - 1, -1, -1):
         item = items[i]
         ...
     → this is the justified range(len()) case

7. Enumerate over a range
   for i, n in enumerate(range(10, 15)):
       # i is 0..4, n is 10..14
   
   Redundant in most cases — enumerate of range
   gives two counters. But useful when you want
   BOTH the enumeration index AND the range value.
   
   Usually you want one or the other:
     for i in range(10, 15):     # just the values
     for i, _ in enumerate(range(10, 15)):  # just indices
   
   enumerate(range(...)) is rare.

The patterns that come up most often:

  for i, item in enumerate(items):        # 90%
  for i, (k, v) in enumerate(d.items()):  # 5%
  for i, (a, b) in enumerate(zip(a, b)):  # 5%

Learn these three.
They cover 99% of enumerate use cases.
The rest are variations.

Don't use range(len()) as a reflex. It's the single most common Python anti-pattern, especially from developers coming from C, Java, or JavaScript. for i in range(len(items)) is nearly always replaceable with for i, item in enumerate(items) — and the replacement is shorter, clearer, faster, and works on more types. The two legitimate uses of range(len()): (1) you need to iterate backwards while modifying the list, (2) you need the index for something other than accessing the sequence. For everything else, use enumerate.

⚠️ Common mistakes

The enumerate mistakes that cost real time.

1. range(len(items))

The anti-pattern. Fix: enumerate(items).

2. Forgetting start=

enumerate(items, 1) works but start=1 is clearer. Fix: enumerate(items, start=1).

3. Wrong unpacking order

for item, i in enumerate(items) — swapped. Fix: for i, item in enumerate(items).

4. Nested tuple unpacking

for i, name, score in enumerate(zip(names, scores)) — wrong. Fix: for i, (name, score) in enumerate(zip(names, scores)).

5. Trying to reverse enumerate

reversed(enumerate(items)) — TypeError. Fix: reversed(list(enumerate(items))).

6. Modifying the list while iterating

Skipped elements, wrong values. Fix: iterate a copy, or build a new list.

7. zip with unequal lengths

Silently truncates. Fix: check lengths, or use zip_longest.

8. Assuming enumerate returns a list

It's an iterator. Fix: list(enumerate(items)) if you need a list.

❓ Frequently asked

How do I loop with an index in Python?

Use enumerate: for i, item in enumerate(items): print(i, item). It yields (index, value) tuples. Start the counter at any number: enumerate(items, start=1). Don't use range(len(items)) — it's slower, less readable, and a known anti-pattern. For parallel iteration over two sequences, use zip.

What is the difference between enumerate and range(len())?

Three differences. (1) enumerate yields (index, value) directly. (2) enumerate works on any iterable (generators, files, sets). (3) enumerate is faster. The range(len()) version is a holdover from C. In modern Python, always use enumerate. The only valid use of range(len()) is when you need to modify the list while iterating.

How do I start enumerate at 1 instead of 0?

Pass start=1: for i, item in enumerate(items, start=1): print(f'{i}. {item}'). The start parameter sets the initial counter value. It doesn't affect iteration order. enumerate(items, start=100) counts 100, 101, 102, ...

Can I iterate two lists at the same time?

Yes — use zip: for a, b in zip(list1, list2). zip yields tuples of corresponding elements. It stops at the shortest. For unequal-length sequences, use itertools.zip_longest with a fillvalue. For index + multiple lists: for i, (a, b) in enumerate(zip(list1, list2)).

Can I modify a list while iterating with enumerate?

Technically yes, but dangerous. enumerate yields the value from the original sequence — modifying the list during iteration can cause skipped elements or wrong values. The safe pattern: iterate over a copy, or build a new list. If you truly need to modify in place, iterate backwards with range(len()).

Does enumerate create a new list?

No — enumerate returns an iterator, not a list. It's lazy: it produces (index, value) tuples one at a time as you iterate. If you need a list, wrap it: list(enumerate(items)). For 99% of code, the iterator is what you want.

🔗 Continue learning

🗺️ Python learning path
The full roadmap from install to first project — every concept in order

Next in this series:

📖 How to iterate a dictionary
keys(), values(), items() — and the set-operation superpower
← All Python problems
Problem solved · Python