Many Python loops need both an item and its position. A manual counter can work, but it adds state that must be initialized, incremented, and kept synchronized. The built-in Python enumerate() function solves this pattern directly by pairing each item from an iterable with a counter.
This guide explains the basic syntax, the start argument, lists, tuples, strings, dictionaries, files, generators, zip(), comprehensions, mutation pitfalls, and practical examples. You will also see when an index is unnecessary and a normal loop is the clearer choice.
If sequences are new to you, review the Python lists guide. For lazy iteration tools beyond enumerate, see the Python itertools guide.
What Does enumerate() Return?
enumerate(iterable, start=0) returns an iterator. Each iteration produces a tuple containing the current count and the next value from the iterable.
languages = ["Python", "JavaScript", "Go"]
for index, language in enumerate(languages):
print(index, language)Output:
0 Python
1 JavaScript
2 GoThe official Python enumerate documentation defines the function and shows its iterator behavior.
Why enumerate() Is Better Than a Manual Counter
A manual counter requires extra code:
index = 0
for language in languages:
print(index, language)
index += 1The enumerate version communicates the intention more clearly and cannot accidentally skip the increment.
for index, language in enumerate(languages):
print(index, language)Use enumerate when the position has meaning. When you only need each value, keep the simpler form:
for language in languages:
print(language)Start Counting at 1
User-facing rankings and numbered menus normally begin at one:
tasks = ["Install Python", "Create a file", "Run the script"]
for number, task in enumerate(tasks, start=1):
print(f"{number}. {task}")The start value can be another integer, including a negative value:
for number, item in enumerate(["a", "b", "c"], start=10):
print(number, item)The counter does not change the actual indexes inside the original list. It only changes the numbers produced by enumerate.
Convert enumerate to a List
Because enumerate is an iterator, you can inspect all pairs by converting it:
pairs = list(enumerate(["red", "green", "blue"], start=1))
print(pairs)
# [(1, 'red'), (2, 'green'), (3, 'blue')]Do this mainly for inspection or when the pairs must be reused. Normal iteration avoids building an extra list in memory.
Use enumerate() with Strings and Tuples
for position, character in enumerate("Python", start=1):
print(position, character)coordinates = (12.5, -3.2, 8.0)
for axis, value in enumerate(coordinates):
print(axis, value)Any iterable can be passed to enumerate, not only lists.
Enumerate Dictionary Items
Iterating directly over a dictionary produces keys. Use items() when you need keys and values, then unpack the nested tuple.
scores = {"Ava": 95, "Noah": 88, "Mia": 91}
for position, (name, score) in enumerate(
scores.items(),
start=1,
):
print(f"{position}. {name}: {score}")Modern dictionaries preserve insertion order. The number represents that iteration order; it is not a permanent dictionary index.
Enumerate a File Line by Line
Files are iterable, so enumerate can add human-friendly line numbers without loading the entire file.
from pathlib import Path
path = Path("notes.txt")
with path.open("r", encoding="utf-8") as file:
for line_number, line in enumerate(file, start=1):
print(f"{line_number:4}: {line.rstrip()}")This pattern is useful in parsers, validators, import tools, and error messages. The Python with statement guide explains why the file is closed automatically.
Combine enumerate() with zip()
zip() combines values from multiple iterables. Enumerate adds a row number to the combined values.
products = ["Keyboard", "Mouse", "Monitor"]
prices = [49.90, 24.50, 219.00]
for row, (product, price) in enumerate(
zip(products, prices),
start=1,
):
print(f"{row}. {product}: ${price:.2f}")The official Python zip documentation notes that normal zip stops when the shortest iterable is exhausted. Use strict=True when different lengths should be treated as an error:
for row, (product, price) in enumerate(
zip(products, prices, strict=True),
start=1,
):
print(row, product, price)Find Positions That Match a Condition
temperatures = [18, 22, 31, 27, 35]
hot_positions = [
index
for index, value in enumerate(temperatures)
if value >= 30
]
print(hot_positions) # [2, 4]The result contains zero-based positions from the original list. Add start=1 when reporting human-facing positions.
Update List Values by Index
Enumerate is useful when the original list must be changed at known positions:
names = [" ava", "NOAH ", " mia "]
for index, name in enumerate(names):
names[index] = name.strip().title()
print(names)A list comprehension is often cleaner when replacing every item:
names = [name.strip().title() for name in names]Use enumerate when the index is also needed for another purpose or only selected positions will be changed.
Do Not Remove Items from the Same List While Iterating
This code can skip values because removing an item shifts later positions:
numbers = [1, 2, 3, 4, 5, 6]
# Avoid this pattern.
for index, number in enumerate(numbers):
if number % 2 == 0:
numbers.pop(index)Create a filtered list instead:
numbers = [number for number in numbers if number % 2 != 0]When deletion by index is required, collect indexes first and remove from the end toward the beginning.
Use enumerate() with Generators
def read_batches():
yield [1, 2]
yield [3, 4]
yield [5]
for batch_number, batch in enumerate(read_batches(), start=1):
print(f"Batch {batch_number}: {batch}")Both the generator and enumerate produce values lazily. The entire sequence does not need to exist in memory.
Understand Iterator Exhaustion
An enumerate object is consumed as it is iterated:
iterator = enumerate(["a", "b", "c"])
print(list(iterator))
print(list(iterator)) # []Create a new enumerate object when you need another pass, or store the resulting pairs in a collection.
Practical Project: Numbered Validation Report
This example validates user records and reports original row numbers.
def validate_user(user):
errors = []
if not user.get("name", "").strip():
errors.append("missing name")
email = user.get("email", "")
if "@" not in email:
errors.append("invalid email")
age = user.get("age")
if not isinstance(age, int) or age < 0:
errors.append("invalid age")
return errors
def build_validation_report(users):
report = []
for row_number, user in enumerate(users, start=1):
errors = validate_user(user)
if errors:
report.append(
f"Row {row_number}: {', '.join(errors)}"
)
return report
users = [
{"name": "Ava", "email": "[email protected]", "age": 29},
{"name": "", "email": "wrong-address", "age": -2},
{"name": "Mia", "email": "[email protected]", "age": 34},
]
for message in build_validation_report(users):
print(message)Returning report messages makes the logic easier to test than printing inside the validator. This approach can be extended with the Python logging module for persistent diagnostics.
Build a Numbered Menu
The terminal quiz game uses enumerate to display choices. Here is a reusable menu function:
def show_menu(options):
for number, option in enumerate(options, start=1):
print(f"{number}. {option}")
show_menu(["Create", "List", "Delete", "Exit"])Type Hints for Enumerated Results
Most functions should accept an iterable and process it directly rather than promising a list of numbered tuples. When a list is truly needed:
from collections.abc import Iterable
from typing import TypeVar
T = TypeVar("T")
def numbered(items: Iterable[T]) -> list[tuple[int, T]]:
return list(enumerate(items, start=1))Common Mistakes
- Using enumerate when the index is never used.
- Confusing the custom counter with the sequence’s real zero-based index.
- Unpacking dictionary items incorrectly.
- Expecting an enumerate object to restart after it has been consumed.
- Removing list elements while iterating forward through the same list.
- Using
range(len(items))when only the item and its position are needed. - Converting a very large enumerate iterator to a list without a reason.
Frequently Asked Questions
Does enumerate() modify the iterable?
No. It reads values and produces counter-value pairs.
Can start be negative?
Yes. It accepts an integer and only controls the produced counter.
Is enumerate memory-efficient?
It is an iterator and produces pairs as needed. Memory use does not grow by creating all pairs in advance.
Can enumerate work with a set?
Yes, but a set is not an appropriate choice when a stable meaningful order is required.
What is the difference between enumerate and zip?
Enumerate adds a counter to one iterable. Zip combines values from two or more iterables. They can be used together.
Should I use range(len(items))?
Use it when you specifically need indexes for index-based operations. Use enumerate when you need both each item and a position.
Conclusion
Python enumerate() removes the bookkeeping of a manual counter while keeping loops readable. Use start=1 for numbered output, unpack nested values carefully, and remember that the returned object is a one-pass iterator.
The best rule is simple: use a normal loop when you need values, enumerate when you need values plus positions, and direct index loops only when the index itself drives the operation.






