The for Loop and Sequence Iteration

Updated

September 7, 2026

The for Loop and Sequence Iteration

After reading this chapter, you will master sequence and collection iteration using the for loop, understand how the Python Iterator Protocol operates under the hood, generate numerical sequences using range(), iterate dictionaries across keys and items, and avoid modifying collections during iteration.

Mental model

Unlike C-style for loops (for (int i=0; i<n; i++)), Python’s for loop is an abstraction over the Iterator Protocol. It operates over any iterable (lists, tuples, dictionaries, sets, generators, strings, open file handles):

How CPython Executes "for item in collection:":
  1. Calls iterator = iter(collection)    ──▶ Calls collection.__iter__()
  2. Enters internal loop:
       try:
           item = next(iterator)          ──▶ Calls iterator.__next__()
           [ Execute Loop Body ]
       except StopIteration:
           break                          ──▶ Terminates cleanly!

Because range(start, stop, step) generates numbers on demand without allocating an actual list in memory, range(1_000_000_000) consumes only 48 bytes of RAM!


Minimal example

Save as for_loop_mechanics.py:

# for_loop_mechanics.py
import sys

def main() -> None:
    # 1. Iterating across a list of cluster nodes
    nodes = ["k8s-node-01", "k8s-node-02", "k8s-node-03"]
    print("--- Node Iteration ---")
    for node in nodes:
        print(f"Active node: {node}")

    # 2. Dictionary iteration via .items()
    node_metrics = {"k8s-node-01": 12.4, "k8s-node-02": 45.1, "k8s-node-03": 88.0}
    print("\n--- Dictionary Key-Value Unpacking ---")
    for hostname, load in node_metrics.items():
        print(f"Host: {hostname:12} | CPU Load: {load:4.1f}%")

    # 3. range() step and stride
    print("\n--- Range Stride (Even ports from 8000 to 8006) ---")
    for port in range(8000, 8008, 2):
        print(f"Allocating port: {port}")

    # Inspecting range memory efficiency
    massive_range = range(1_000_000_000)
    print(f"\nMemory size of range(1,000,000,000): {sys.getsizeof(massive_range)} bytes")

if __name__ == "__main__":
    main()

Run via uv run python for_loop_mechanics.py:

--- Node Iteration ---
Active node: k8s-node-01
Active node: k8s-node-02
Active node: k8s-node-03

--- Dictionary Key-Value Unpacking ---
Host: k8s-node-01  | CPU Load: 12.4%
Host: k8s-node-02  | CPU Load: 45.1%
Host: k8s-node-03  | CPU Load: 88.0%

--- Range Stride (Even ports from 8000 to 8006) ---
Allocating port: 8000
Allocating port: 8002
Allocating port: 8004
Allocating port: 8006

Memory size of range(1,000,000,000): 48 bytes

Worked examples

Case 1: Deconstructing the Iterator Protocol Manually

To understand what Python does behind the scenes during a for loop, you can execute the exact same protocol manually using iter() and next():

# manual_iterator_demo.py
def demonstrate_manual_iteration() -> None:
    items = ["auth-svc", "billing-svc", "gateway-svc"]

    # 1. Obtain iterator object
    iterator = iter(items)
    print("Iterator object created:", iterator)

    # 2. Emulate the for-loop using while and StopIteration
    print("\nExecuting manual loop:")
    while True:
        try:
            item = next(iterator)
            print(f"  Pulled item from iterator: {item}")
        except StopIteration:
            print("  StopIteration raised! CPython caught it and terminated loop.")
            break

if __name__ == "__main__":
    demonstrate_manual_iteration()

Run:

uv run python manual_iterator_demo.py

Output:

Iterator object created: <list_iterator object at ...>

Executing manual loop:
  Pulled item from iterator: auth-svc
  Pulled item from iterator: billing-svc
  Pulled item from iterator: gateway-svc
  StopIteration raised! CPython caught it and terminated loop.

Case 2: File Iteration Line by Line (Constant Memory)

An open file object is an iterable that yields one line at a time on demand. Iterating directly over the file handle reads from disk incrementally without ever loading the full file into RAM:

# file_line_streamer.py
import tempfile
from pathlib import Path

def process_large_logfile(log_path: Path) -> int:
    error_count = 0
    # Opening the file returns an iterable stream
    with log_path.open("r", encoding="utf-8") as stream:
        for line in stream:
            if "[ERROR]" in line:
                error_count += 1
    return error_count

if __name__ == "__main__":
    with tempfile.TemporaryDirectory() as tmpdir:
        dummy_log = Path(tmpdir) / "cluster.log"
        dummy_log.write_text(
            "2026-09-07 [INFO] Started\n"
            "2026-09-07 [ERROR] Database timeout\n"
            "2026-09-07 [INFO] Retrying\n"
            "2026-09-07 [ERROR] Auth rejected\n"
        )

        total_errors = process_large_logfile(dummy_log)
        print(f"Total error lines identified: {total_errors}")

Run:

uv run python file_line_streamer.py

Output:

Total error lines identified: 2

Pitfalls

Pitfall 1: Modifying a Collection While Iterating Over It

Mutating a list while iterating over it causes internal index shifts that skip items or loop indefinitely:

# THE BUG:
numbers = [1, 2, 3, 4, 5, 6]
for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)  # Skips elements during iteration!
print(numbers)  # [1, 3, 5] by coincidence, but elements like 4 or 6 get skipped on other data!

# THE FIX: Iterate over a shallow copy or use a list comprehension
numbers = [n for n in numbers if n % 2 != 0]

For dictionaries, attempting to add or remove keys during iteration immediately crashes with RuntimeError: dictionary changed size during iteration.

Pitfall 2: The range(len(sequence)) Anti-Pattern

Programmers coming from C or Java frequently write for i in range(len(items)): val = items[i]. This is unpythonic, verbose, and error-prone. Iterate directly over items:

# ANTI-PATTERN:
for i in range(len(servers)):
    print(servers[i])

# IDIOMATIC:
for server in servers:
    print(server)

If you need the index, use enumerate(servers) (covered in chapter 6).


Exercises

  1. Write a for loop that iterates over a dictionary and prints only the keys whose values exceed 100.
  2. Generate all multiples of 5 between 50 and 100 in reverse order using range().
  3. Demonstrate iterating over a string character by character to count how many vowels it contains.
  4. Implement a custom class with __iter__ and __next__ that yields squares of numbers up to a maximum limit.

Further reading

  • Python Language Reference: The for statement.
  • Python Data Model: The Iterator Protocol (__iter__, __next__).