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.

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 error
for 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()
range does not build a list. It generates each value as the loop asks for it, so range(10_000_000) uses a few dozen bytes rather than hundreds of megabytes. Wrap it in list() 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=9

The 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)) where enumerate was wanted.
  • Expecting range(1, 10) to include 10.
  • Modifying a list while looping over it.
  • Forgetting that zip silently 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 + 1 instead of using enumerate.

Best practices

  • Iterate over items directly. Use range only for genuine number sequences.
  • Use enumerate for indexes and zip for 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

  1. Print the multiplication table for 1 to 12 as a formatted grid.
  2. Given two lists of unequal length, pair them safely and report how many items were dropped.
  3. Print a numbered list of names starting at 1 without using range.
  4. Explain why removing even numbers while looping is unsafe, and give two correct alternatives.
  5. 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

The while Loop

while repeats for as long as a condition holds. Use it when the number of repetitions is unknown, and make sure something inside the loop can make the...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.