The for Loop and range()
A Python for loop walks over the items of a collection directly. range() supplies numbers when you genuinely need a counter, which is less often than you expect.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
for iterates over items, not indexes
names = ["Meera", "Arun", "Sara"]
for name in names:
print(name)There is no counter, no length check and no bounds error to get wrong. Python's for is a "for each" loop, and it works on anything iterable: lists, tuples, strings, sets, dictionaries, files, ranges and generators.
for character in "Python":
print(character, end=" ")
print()
for number in (10, 20, 30):
print(number)
for line in ["first", "second"]:
print(line.upper())range
range(start, stop, step) produces numbers on demand. start defaults to 0, step defaults to 1, and stop is always excluded.
print(list(range(5))) # [0, 1, 2, 3, 4]
print(list(range(2, 7))) # [2, 3, 4, 5, 6]
print(list(range(0, 10, 3))) # [0, 3, 6, 9]
print(list(range(10, 0, -2))) # [10, 8, 6, 4, 2]
print(list(range(5, 5))) # [] - empty, not an errorfor i in range(5):
print(i, end=" ") # 0 1 2 3 4
print()
for i in range(1, 11):
print(i * i, end=" ") # the first ten squares
print()rangedoes not build a list. It generates each value as the loop asks for it, sorange(10_000_000)uses a few dozen bytes rather than hundreds of megabytes. Wrap it inlist()only when you actually need all the values at once.
When you need the index too
names = ["Meera", "Arun", "Sara"]
# Not idiomatic
for i in range(len(names)):
print(i, names[i])
# Idiomatic
for index, name in enumerate(names):
print(index, name)
# Counting from 1 for display
for position, name in enumerate(names, start=1):
print(f"{position}. {name}")range(len(...)) is a sign that enumerate was wanted. Reach for the index only when you need to modify the list in place by position.
Looping over several sequences together
names = ["Meera", "Arun", "Sara"]
scores = [92, 78, 85]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# zip stops at the shortest sequence
print(list(zip([1, 2, 3], ["a", "b"]))) # [(1, 'a'), (2, 'b')]
# Insist that they are the same length
for name, score in zip(names, scores, strict=True): # Python 3.10+
print(name, score)Looping over a dictionary
ages = {"Meera": 27, "Arun": 31, "Sara": 24}
for key in ages: # keys by default
print(key)
for value in ages.values():
print(value)
for key, value in ages.items(): # the usual form
print(f"{key} is {value}")Nested loops
for row in range(1, 4):
for column in range(1, 4):
print(f"{row}x{column}={row * column}", end=" ")
print()1x1=1 1x2=2 1x3=3
2x1=2 2x2=4 2x3=6
3x1=3 3x2=6 3x3=9The inner loop runs completely for every single iteration of the outer loop. Two nested loops over n items each perform n * n iterations, which matters once n grows.
Iterating over a grid
grid = [
[1, 2, 3],
[4, 5, 6],
]
for row in grid:
for value in row:
print(value, end=" ")
print()
# With coordinates
for r, row in enumerate(grid):
for c, value in enumerate(row):
print(f"({r},{c})={value}", end=" ")
print()Modifying a list while looping over it
numbers = [1, 2, 3, 4, 5, 6]
# WRONG: removing shifts the remaining items and the loop skips some
for n in numbers:
if n % 2 == 0:
numbers.remove(n)
print(numbers) # [1, 3, 5] here, but the technique is unreliable
# Correct: build a new list
numbers = [1, 2, 3, 4, 5, 6]
numbers = [n for n in numbers if n % 2 != 0]
print(numbers) # [1, 3, 5]
# Or loop over a copy if you must mutate in place
numbers = [1, 2, 3, 4, 5, 6]
for n in numbers[:]:
if n % 2 == 0:
numbers.remove(n)Never add to or remove from a collection you are currently iterating. Build a new one, or iterate over a copy.
Accumulating results
scores = [92, 78, 85, 60]
total = 0
for score in scores:
total += score
print(total / len(scores)) # 78.75
# Built ins do this better
print(sum(scores) / len(scores))
print(max(scores), min(scores), len(scores))When a loop only sums, counts, finds the largest or builds a list, a built in function or a comprehension is usually clearer and faster.
Common mistakes
- Using
range(len(items))whereenumeratewas wanted. - Expecting
range(1, 10)to include 10. - Modifying a list while looping over it.
- Forgetting that
zipsilently stops at the shortest input. - Reusing the loop variable after the loop; it survives, holding the last value.
- Building an index by hand with
i = i + 1instead of usingenumerate.
Best practices
- Iterate over items directly. Use
rangeonly for genuine number sequences. - Use
enumeratefor indexes andzipfor parallel sequences. - Use
.items()when a loop needs both key and value. - Prefer a comprehension when the loop only builds a list.
- Keep nesting to two levels; deeper usually means a function is waiting to be extracted.
Practice
- Print the multiplication table for 1 to 12 as a formatted grid.
- Given two lists of unequal length, pair them safely and report how many items were dropped.
- Print a numbered list of names starting at 1 without using
range. - Explain why removing even numbers while looping is unsafe, and give two correct alternatives.
- Sum only the values at even indexes of a list, using
enumerate.
Conclusion
Loop over the collection, not over its indexes. When you need positions use enumerate, when you need pairs use zip, and when you genuinely need numbers use range, remembering that stop is always excluded.