Python For Loop with Index: enumerate(), start and Pitfalls

To get the index in a Python for loop, wrap whatever you are looping over in enumerate():

for index, fruit in enumerate(fruits):
    print(index, fruit)        # 0 apple, 1 pear, 2 plum

It hands back the position and the item on every pass, so there is no counter to maintain and no square brackets to get wrong.

Below: the start argument, what happens with dictionaries, and the bug that appears when you remove items while looping. Everything ran on Python 3.12.5.

Diagram comparing a plain Python for loop with an enumerate loop that returns both the index and the item
enumerate() adds the position without changing anything else.

Using enumerate to get the index

The loop unpacks two values instead of one:

fruits = ["apple", "pear", "plum"]

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

print()
print("enumerate hands back a position and an item on every pass,")
print("so there is no indexing and nothing to count yourself.")

Output:

0 apple
1 pear
2 plum

enumerate hands back a position and an item on every pass,
so there is no indexing and nothing to count yourself.
Command Prompt showing a Python for loop using enumerate to print the index and the item
enumerate() returns a position and an item on each pass.

The names are yours to pick. for i, fruit in ... is the common short form, and position or row_number is clearer in longer code.

Python loop index: enumerate vs a counter vs range(len())

Three ways to reach the same output:

fruits = ["apple", "pear", "plum"]

print("counting manually:")
index = 0
for fruit in fruits:
    print("  ", index, fruit)
    index += 1

print()
print("looping over positions and looking each item up:")
for i in range(len(fruits)):
    print("  ", i, fruits[i])

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

print()
print("all three print the same thing. The third has no counter to")
print("forget to increment and no index to get wrong.")

Output:

counting manually:
   0 apple
   1 pear
   2 plum

looping over positions and looking each item up:
   0 apple
   1 pear
   2 plum

with enumerate:
   0 apple
   1 pear
   2 plum

all three print the same thing. The third has no counter to
forget to increment and no index to get wrong.
Command Prompt comparing a manual counter, range(len()) and enumerate for looping with an index in Python
The same result three ways; only the last one has nothing to get wrong.

The manual counter works until someone adds a continue above the index += 1 line, at which point the numbering quietly breaks.

range(len(fruits)) is safe but noisy, and every item access goes through an index. What for i in range actually means covers that construct on its own.

Start the loop index at 1

enumerate() takes a second argument:

tasks = ["write", "review", "publish"]

print("default, counting from 0:")
for i, task in enumerate(tasks):
    print("  ", i, task)

print()
print("start=1, for anything a person will read:")
for number, task in enumerate(tasks, start=1):
    print("  ", number, task)

print()
print("start can be any integer:")
print(" ", list(enumerate(tasks, 100)))
print()
print("start only changes the COUNTER. It does not skip items.")

Output:

default, counting from 0:
   0 write
   1 review
   2 publish

start=1, for anything a person will read:
   1 write
   2 review
   3 publish

start can be any integer:
  [(100, 'write'), (101, 'review'), (102, 'publish')]

start only changes the COUNTER. It does not skip items.

Use start=1 whenever the number is shown to a person, such as step numbers or line numbers. Leave it at zero when the number is used to index back into the list.

It only shifts the counter. The items are untouched, so nothing is skipped.

enumerate returns an iterator, not a list of indexes

This catches people who try to reuse it:

fruits = ["apple", "pear", "plum"]

pairs = enumerate(fruits)
print("enumerate(fruits) ->", pairs)
print("type              ->", type(pairs))
print()
print("it produces values on demand, and only once:")
print("  first list(pairs)  ->", list(pairs))
print("  second list(pairs) ->", list(pairs), " <- already used up")
print()
print("call enumerate() again, or keep the list, if you need two passes:")
kept = list(enumerate(fruits))
print("  kept ->", kept)
print("  kept again ->", kept)

Output:

enumerate(fruits) -> <enumerate object at 0x0000017BC0A05B70>
type              -> <class 'enumerate'>

it produces values on demand, and only once:
  first list(pairs)  -> [(0, 'apple'), (1, 'pear'), (2, 'plum')]
  second list(pairs) -> []  <- already used up

call enumerate() again, or keep the list, if you need two passes:
  kept -> [(0, 'apple'), (1, 'pear'), (2, 'plum')]
  kept again -> [(0, 'apple'), (1, 'pear'), (2, 'plum')]

It produces pairs on demand, which is why looping over a huge list costs no extra memory. The cost is that a second pass finds nothing left.

Call enumerate() again for another pass, or store list(enumerate(items)) if you genuinely need it twice.

Loop with index over strings, tuples and dictionaries

Anything iterable works, but dictionaries behave in a way worth knowing:

print("a string gives characters:")
print(" ", list(enumerate("abc")))
print()
print("a tuple works the same:")
print(" ", list(enumerate(("x", "y"))))
print()

scores = {"ann": 90, "bob": 85}
print("a dictionary gives KEYS only, which catches people out:")
print(" ", list(enumerate(scores)))
print()
print("use .items() when you want the values too:")
print(" ", list(enumerate(scores.items())))
print()
print("and unpack the pair with its own brackets:")
for i, (name, score) in enumerate(scores.items()):
    print(f"   {i}  {name} scored {score}")

Output:

a string gives characters:
  [(0, 'a'), (1, 'b'), (2, 'c')]

a tuple works the same:
  [(0, 'x'), (1, 'y')]

a dictionary gives KEYS only, which catches people out:
  [(0, 'ann'), (1, 'bob')]

use .items() when you want the values too:
  [(0, ('ann', 90)), (1, ('bob', 85))]

and unpack the pair with its own brackets:
   0  ann scored 90
   1  bob scored 85
Command Prompt showing that enumerate on a Python dictionary yields only the keys unless items is used
Looping a dictionary yields keys, so enumerate(d) numbers the keys.

enumerate(scores) numbers the keys because that is what looping a dictionary gives you. Add .items() to get the values as well.

The pair then needs its own brackets: for i, (name, score) in .... Without them Python cannot tell how many values to unpack. There is more in iterating through a dictionary.

Loop with index over two lists using zip

Numbering two parallel lists at once:

names = ["ann", "bob"]
ages = [30, 40]

print("zip pairs two lists; enumerate numbers the pairs:")
for i, (name, age) in enumerate(zip(names, ages)):
    print(f"  {i}  {name} is {age}")

print()
print("the inner brackets matter. Without them Python cannot tell")
print("how many values it is unpacking:")
print(" ", list(enumerate(zip(names, ages))))

Output:

zip pairs two lists; enumerate numbers the pairs:
  0  ann is 30
  1  bob is 40

the inner brackets matter. Without them Python cannot tell
how many values it is unpacking:
  [(0, ('ann', 30)), (1, ('bob', 40))]

zip makes the pairs and enumerate numbers them, so the loop variable is i plus a tuple. The inner brackets are what split that tuple back out.

Why removing items inside the loop skips things

This is the bug the index makes possible:

numbers = [1, 2, 4, 5]

for i, value in enumerate(numbers):
    if value % 2 == 0:
        numbers.remove(value)

print("removing even numbers while looping over the list:")
print("  result   ->", numbers)
print("  expected ->", [n for n in [1, 2, 4, 5] if n % 2])
print("  the 4 was skipped entirely")
print()
print("removing an item shifts everything left, but the counter still")
print("moves right, so one item gets stepped over each time.")
print()

print("safe: build a new list instead")
print("  ", [n for n in [1, 2, 4, 5] if n % 2])
print()
print("safe: loop over a copy if you must remove in place")
numbers = [1, 2, 4, 5]
for value in numbers[:]:
    if value % 2 == 0:
        numbers.remove(value)
print("  ", numbers)

Output:

removing even numbers while looping over the list:
  result   -> [1, 4, 5]
  expected -> [1, 5]
  the 4 was skipped entirely

removing an item shifts everything left, but the counter still
moves right, so one item gets stepped over each time.

safe: build a new list instead
   [1, 5]

safe: loop over a copy if you must remove in place
   [1, 5]
Command Prompt showing that removing items from a Python list while looping over it causes items to be skipped
Removing while looping shifts the list left, so the 4 is never examined.

Removing an item moves everything after it one place left, while the counter carries on to the right. One item gets stepped over for each removal.

It is worse than it looks, because the result is sometimes correct by accident. With [1, 2, 3, 4, 5, 6] the same code gives the right answer, which is exactly how the bug survives testing.

Build a new list with a comprehension, or loop over a copy with items[:] if you must remove in place. Splitting a list covers the related case of processing in chunks.

Changing items through the index

The index is what lets you write back into the list:

prices = [10, 20, 30]

for i, price in enumerate(prices):
    prices[i] = price * 1.1

print("updating each item in place through its index:")
print(" ", [round(p, 2) for p in prices])
print()
print("assigning to `price` instead would do nothing, because it is")
print("only a name for the value, not a slot in the list:")

prices = [10, 20, 30]
for i, price in enumerate(prices):
    price = price * 1.1
print(" ", prices, " <- unchanged")

Output:

updating each item in place through its index:
  [11.0, 22.0, 33.0]

assigning to `price` instead would do nothing, because it is
only a name for the value, not a slot in the list:
  [10, 20, 30]  <- unchanged

prices[i] = ... changes the list. Assigning to the loop variable only rebinds a local name, and the list never sees it.

Finding the index of a particular value

A different question, and a different tool:

fruits = ["apple", "pear", "plum", "pear"]

print("to find WHERE a value sits, use .index():")
print("  fruits.index('pear') ->", fruits.index("pear"))
print()
print("it only reports the first match:")
print("  every position of 'pear' ->", [i for i, f in enumerate(fruits) if f == "pear"])
print()
print("and it raises if the value is absent:")
try:
    fruits.index("kiwi")
except ValueError as err:
    print("  fruits.index('kiwi') -> ValueError:", err)
print()
print("a safe version:")
target = "kiwi"
position = next((i for i, f in enumerate(fruits) if f == target), None)
print(f"  position of {target!r} ->", position)

Output:

to find WHERE a value sits, use .index():
  fruits.index('pear') -> 1

it only reports the first match:
  every position of 'pear' -> [1, 3]

and it raises if the value is absent:
  fruits.index('kiwi') -> ValueError: 'kiwi' is not in list

a safe version:
  position of 'kiwi' -> None

.index() searches for a value and returns the first position, raising ValueError when it is absent. To find every match, filter with enumerate instead.

The next(...) form returns None rather than raising, which suits code that expects misses. Getting the index of an element goes through all of these.

Is enumerate faster than range(len())?

No, and it is worth saying so plainly:

import timeit

setup = "data = list(range(2_000_000))"

enum_time = timeit.timeit("for i, v in enumerate(data): pass", setup, number=1)
range_time = timeit.timeit("for i in range(len(data)): v = data[i]", setup, number=1)

print("one pass over 2,000,000 items:")
print(f"  enumerate(data)        {enum_time:.3f}s")
print(f"  range(len(data))       {range_time:.3f}s")
print()
print("they are the same speed. Choose enumerate because it reads")
print("better and removes a chance to make an indexing mistake,")
print("not because it is faster.")

Output:

one pass over 2,000,000 items:
  enumerate(data)        0.043s
  range(len(data))       0.042s

they are the same speed. Choose enumerate because it reads
better and removes a chance to make an indexing mistake,
not because it is faster.
Command Prompt timing enumerate against range with len over two million items in Python
Over two million items the two are the same speed.

Pick enumerate because it says what it means and removes a chance to make an indexing mistake. Performance is not the argument.

Python for loop index quick reference

What you needUse
Index and itemfor i, x in enumerate(items):
Numbering from 1enumerate(items, start=1)
Index and both parts of a dictenumerate(d.items())
Two lists at onceenumerate(zip(a, b))
Position of a known valueitems.index(value)
Every position of a value[i for i, x in enumerate(items) if x == value]
To modify while iteratingA comprehension, or loop over items[:]

Related guides to read next:

Frequently asked questions

How do I get the index in a Python for loop?

Use enumerate(): for i, item in enumerate(items):. It returns the position and the item together on every pass.

How do I make the index start at 1?

Pass the start argument: enumerate(items, start=1). It shifts the counter only, so no items are skipped.

Is enumerate faster than range(len())?

No. Over two million items they take the same time. enumerate wins on readability and on removing a chance to index wrongly.

Why does enumerate on a dictionary give only the keys?

Because looping a dictionary yields its keys. Use enumerate(d.items()) and unpack with for i, (key, value) in ... to get both.

Why are items skipped when I remove them in the loop?

Removing shifts the remaining items left while the counter moves right, so one is stepped over per removal. Build a new list, or iterate over a copy with items[:].

Can I loop over an enumerate object twice?

No. It is a one-pass iterator and comes back empty the second time. Call enumerate() again, or keep list(enumerate(items)).

How do I find the index of a specific value?

items.index(value) gives the first position and raises ValueError if absent. The built-in is described in the Python enumerate documentation.