Python For Loop

A for loop lets you run a block of code once for every item in a collection — a list of names, the characters of a string, the lines of a file, or a range of numbers. Instead of manually tracking an index like you would in many other languages, Python’s for loop hands you each item directly, which makes code shorter, safer, and easier to read. It is the single most common way to process data in Python, so understanding exactly how it works — not just how to use it — pays off constantly.

Overview: How the For Loop Works

Python’s for loop is fundamentally a for-each loop. It does not count from a starting number to an ending number like a C-style for (i = 0; i < n; i++) loop. Instead, it walks over the elements produced by an iterable, one at a time, until the iterable is exhausted.

To understand what happens internally, you need two related concepts:

  • Iterable — any object that can produce a sequence of values, because it implements a special method called __iter__(). Lists, tuples, strings, dictionaries, sets, files, and range objects are all iterables.
  • Iterator — the object actually doing the work of producing values one at a time. It implements __next__(), which returns the next value each time it’s called, and raises a special StopIteration exception when there are no values left.

When you write for item in iterable:, Python performs roughly these steps behind the scenes:

  1. Call iter(iterable) to obtain an iterator object.
  2. Call next() on that iterator to get the next value.
  3. Assign the returned value to the loop variable (item) and execute the loop body.
  4. Repeat step 2 and 3 until next() raises StopIteration.
  5. Catch that StopIteration silently and exit the loop — you never see the exception yourself.

This is why almost anything can be looped over with for: as long as an object knows how to hand out its values one at a time, Python doesn’t care whether it’s backed by a list in memory, a file being read line by line, or numbers being generated on the fly (as with range or a generator function). The loop variable is an ordinary variable — after the loop ends, it still holds the last value it was assigned, and it can be unpacked into multiple names at once, which is how for key, value in some_dict.items(): works.

Python’s for loop also supports an optional else clause. The else block runs only if the loop finishes normally — that is, it was not stopped early by a break statement. This is a lesser-known but genuinely useful feature for “search and report” patterns, shown in Example 3 below.

Syntax

for item in iterable:
    # loop body — runs once per item, with item bound to the current value
    print(item)
else:
    # optional — runs once, after the loop ends WITHOUT a break
    print("Loop finished")
Part Meaning
for Keyword that starts the loop
item The loop (target) variable; can also be a tuple like a, b for unpacking
in iterable Any iterable: list, tuple, string, dict, set, range, file, generator, etc.
: Required; introduces the indented loop body
break Exits the loop immediately, skipping the rest of the body and any else
continue Skips the rest of the current iteration and moves to the next item
else Optional block that runs only if the loop was not exited via break

Two built-in functions appear alongside for constantly: range(start, stop, step), which generates a sequence of numbers without building a list in memory, and enumerate(iterable, start=0), which pairs each item with its index. There’s also zip() for looping over several sequences together, covered under Best Practices.

Examples

Example 1: Looping over a list

fruits = ["apple", "banana", "cherry", "date"]
for fruit in fruits:
    print(f"I like {fruit}")

Output:

I like apple
I like banana
I like cherry
I like date

Each pass through the loop binds fruit to the next string in the list, in order, and the f-string embeds it directly into the printed message. No index variable is needed anywhere.

Example 2: range(), enumerate(), and accumulating a result

scores = [88, 92, 79, 95, 60]
total = 0
for index, score in enumerate(scores, start=1):
    total += score
    print(f"Student {index}: {score} points")

average = total / len(scores)
print(f"Average score: {average:.2f}")

Output:

Student 1: 88 points
Student 2: 92 points
Student 3: 79 points
Student 4: 95 points
Student 5: 60 points
Average score: 82.80

enumerate(scores, start=1) produces pairs like (1, 88), (2, 92), and so on, which the loop unpacks directly into index and score. Meanwhile total is a classic accumulator pattern: it starts at zero outside the loop and grows on each iteration, ending up with the sum used to compute the average after the loop finishes.

Example 3: Nested logic with break and for-else

def is_prime(n: int) -> bool:
    if n < 2:
        return False
    for divisor in range(2, int(n ** 0.5) + 1):
        if n % divisor == 0:
            print(f"{n} is divisible by {divisor}")
            break
    else:
        print(f"{n} is prime")
        return True
    return False


for number in [15, 17, 22, 23]:
    is_prime(number)

Output:

15 is divisible by 3
17 is prime
22 is divisible by 2
23 is prime

The inner loop tests possible divisors up to the square root of n. If it finds one, it prints the divisor and immediately breaks — which skips the attached else block entirely. If the loop runs to completion without ever breaking (no divisor was found), the else block runs, reporting that the number is prime. This for...else pairing is the classic "search for something; if you never find it, do X" pattern, and it reads more cleanly than tracking a separate "found" boolean flag.

Under the Hood: Step by Step

You can watch the iterator protocol in action by driving it manually instead of letting for do it for you:

numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))

Output:

10
20
30

Here, iter(numbers) creates a fresh list-iterator object, and each call to next(iterator) advances it by one position, exactly as a for loop would do internally. If you called next(iterator) a fourth time, it would raise StopIteration — a normal for loop simply catches that exception for you and stops. This is also why you can only loop over a plain iterator (as opposed to a re-iterable container like a list) once: once it's exhausted, calling iter() on the same iterator object just returns itself, still empty.

Common Mistakes

Mistake 1: Modifying a list while iterating over it

numbers = [2, 4, 6, 8]
for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)
print(numbers)

Output:

[4, 8]

This looks like it should empty the list, but it doesn't. Internally, the for loop tracks a numeric position and asks the list for the item at that position each time. Removing an item shifts every later item one slot to the left, so the loop's position skips over the very next element — the loop silently steps over values. The fix is to never mutate the list you're iterating over; instead, build a new list or iterate over a copy:

numbers = [2, 4, 6, 8]
numbers = [n for n in numbers if n % 2 != 0]
print(numbers)

Output:

[]

A list comprehension builds a brand-new list from scratch, so there's no shifting-index problem. If you must remove items in place, loop over an explicit copy instead, e.g. for n in numbers[:]:.

Mistake 2: Assuming range() is inclusive of its stop value

for i in range(1, 10):
    print(i)

Output:

1
2
3
4
5
6
7
8
9

New Python developers often expect this to print through 10, since it "looks like" 1 to 10. But range(start, stop) always excludes stop — it generates values up to, but not including, that number. To print 1 through 10 inclusive, the stop value needs to be one higher:

for i in range(1, 11):
    print(i)

Output:

1
2
3
4
5
6
7
8
9
10

A useful mental model: range(a, b) produces exactly b - a values, starting at a.

Best Practices

  • Iterate directly over a collection (for fruit in fruits:) instead of indexing with for i in range(len(fruits)): fruits[i] — it's shorter and less error-prone.
  • When you need the index and the value, use enumerate() rather than manually maintaining a counter variable.
  • Never add to or remove from a list while looping over that same list; build a new list (often with a comprehension) or loop over a copy (list[:]) instead.
  • Use zip(list_a, list_b) to loop over two or more sequences in parallel instead of indexing each one separately.
  • Reach for a for...else when your loop is a search: it removes the need for a manual "found" flag.
  • Prefer a list, set, or dict comprehension over a manual accumulator loop when you're just transforming or filtering data into a new collection — it's usually more readable and often faster.
  • Give the loop variable a meaningful, singular name (for student in students:, not for x in students:) so the body reads naturally.
  • For very large or infinite sequences, prefer generators/iterators over building full lists up front, since for only ever needs one value at a time.

Practice Exercises

  1. Write a for loop that prints the square of every integer from 1 to 10 (inclusive), one per line.
  2. Given words = ["kiwi", "pomegranate", "fig", "blueberry"], use a for loop with enumerate() to print each word's index and text, then determine and print which word is the longest.
  3. Write a function that takes a list of numbers and uses a for...else loop to check whether any of them is negative. If it finds one, print the number and break; if it never finds one, let the else clause print "No negative numbers found".

Summary

  • Python's for loop is a for-each loop: it walks over the values an iterable produces, rather than counting indices.
  • Under the hood, for calls iter() to get an iterator, then repeatedly calls next() until StopIteration is raised.
  • range() generates numbers on demand and excludes its stop value; enumerate() pairs items with their index.
  • An optional else clause runs only when the loop completes without hitting break — ideal for search patterns.
  • Never mutate a list while iterating over it directly; iterate over a copy or build a new collection instead.
  • Prefer comprehensions and enumerate()/zip() over manual index bookkeeping for cleaner, more idiomatic Python.