Skip to content

Python Loops — Language Specification

1. Spec Reference

  • Primary source: Python Language Reference, §8.3 — The for statement https://docs.python.org/3/reference/compound_stmts.html#the-for-statement
  • §8.2 — The while statement https://docs.python.org/3/reference/compound_stmts.html#the-while-statement
  • §7.9 — break statement: https://docs.python.org/3/reference/simple_stmts.html#the-break-statement
  • §7.10 — continue statement: https://docs.python.org/3/reference/simple_stmts.html#the-continue-statement
  • Iterator protocol: §3.4.2 — https://docs.python.org/3/reference/datamodel.html#object.iter
  • Generator expressions: §6.2.8 — https://docs.python.org/3/reference/expressions.html#generator-expressions
  • Python 3.12 standard: All rules here apply to CPython 3.12 unless otherwise noted.

2. Formal Grammar (EBNF)

2.1 while Statement

while_stmt ::= "while" assignment_expression ":" suite
               ["else" ":" suite]

2.2 for Statement

for_stmt   ::= "for" target_list "in" starred_list ":" suite
               ["else" ":" suite]

2.3 break Statement

break_stmt ::= "break"

2.4 continue Statement

continue_stmt ::= "continue"

2.5 Generator Expression (inline iteration)

generator_expression ::= "(" expression comp_for ")"
comp_for             ::= ["async"] "for" target_list "in" or_test [comp_iter]
comp_iter            ::= comp_for | comp_if
comp_if              ::= "if" or_test [comp_iter]

2.6 Comprehensions

list_display        ::= "[" [starred_list | comprehension] "]"
set_display         ::= "{" (starred_list | comprehension) "}"
dict_display        ::= "{" [key_datum_list | dict_comprehension] "}"
dict_comprehension  ::= expression ":" expression comp_for
comprehension       ::= assignment_expression comp_for

3. Core Rules and Constraints

3.1 for Loop Semantics

  • The iterable expression is evaluated once before the loop begins.
  • Each iteration calls __next__() on the iterator; StopIteration ends the loop.
  • Modifying a mutable sequence during iteration over it is undefined behavior (see §6).
  • The loop variable(s) remain bound after the loop; they hold the value from the last iteration.
  • If the iterable is empty, the for body is not executed and the else clause (if present) runs.

3.2 while Loop Semantics

  • Condition is evaluated before each iteration.
  • If condition is falsy on first check, the body is never executed.
  • The else clause executes after the loop exits normally (condition became falsy).
  • The else clause does not execute if the loop exits via break.

3.3 else Clause on Loops

  • Both for and while support an else clause.
  • The else clause runs if the loop completed without break being executed.
  • Useful for "search" patterns: execute else when the searched item was not found.
  • continue does not suppress the else clause.

3.4 break Statement

  • Immediately exits the innermost for or while loop.
  • The loop's else clause is skipped when break exits the loop.
  • Can only appear inside a loop; break outside a loop is a SyntaxError.
  • break in a try block: finally clause still executes before the loop is exited.

3.5 continue Statement

  • Skips the rest of the current iteration and continues with the next.
  • For for loops: advances the iterator.
  • For while loops: re-evaluates the condition.
  • The else clause is unaffected by continue.
  • Can only appear inside a loop; continue outside a loop is a SyntaxError.

3.6 Loop Variable Scope

  • Loop variables in for loops are not scoped to the loop; they persist in the enclosing scope.
  • In comprehensions (Python 3), the iteration variable is local to the comprehension.
  • for i in range(3): passi is bound to 2 after the loop.

3.7 Nested Loops

  • break and continue only affect the innermost enclosing loop.
  • To break out of multiple nested loops, use a flag variable, a function with return, or for...else patterns.

4. Type Rules (Dunder Methods and Protocols)

4.1 Iterator Protocol

# An iterable defines:
object.__iter__(self) -> iterator
# Returns an iterator object.

# An iterator defines:
object.__iter__(self) -> self        # iterators must return themselves
object.__next__(self) -> value
# Returns next value or raises StopIteration when exhausted.

4.2 for Loop Protocol Steps

  1. Call iter(obj) → calls obj.__iter__() → returns an iterator it.
  2. Repeatedly call next(it) → calls it.__next__().
  3. When StopIteration is raised, the loop terminates normally.

4.3 __reversed__ Protocol

object.__reversed__(self) -> iterator
# Called by reversed(); if not defined, reversed() falls back to
# using __len__ and __getitem__ with decreasing indices.

4.4 Async Iteration Protocol (PEP 492)

object.__aiter__(self) -> async_iterator
object.__anext__(self) -> coroutine  # raises StopAsyncIteration when done
# Used by 'async for' loops in async functions.

4.5 Generator Protocol

# A generator function contains 'yield'; calling it returns a generator object.
# Generator objects implement the iterator protocol automatically.
generator.__next__()   # equivalent to next(generator)
generator.send(value)  # sends value to yield expression
generator.throw(type)  # raises exception at the yield point
generator.close()      # raises GeneratorExit at the yield point

5. Behavioral Specification

5.1 for Loop Execution Steps

  1. Evaluate the iterable expression → iter_obj.
  2. Call iter(iter_obj) to get iterator it.
  3. Start of each iteration: call next(it).
  4. If StopIteration: exit loop, run else (if present), done.
  5. Otherwise: bind the returned value to the target(s), execute loop body.
  6. After loop body: go to step 3.
  7. break encountered: exit immediately, skip else.
  8. continue encountered: go to step 3 immediately.

5.2 while Loop Execution Steps

  1. Evaluate condition.
  2. If falsy: run else (if present), done.
  3. Execute loop body.
  4. break encountered: exit immediately, skip else.
  5. continue encountered: go to step 1.
  6. Otherwise: go to step 1.

5.3 Comprehension Scoping (Python 3)

  • List, set, dict comprehensions and generator expressions are compiled as nested functions.
  • The iteration variable is local to the comprehension scope.
  • The outermost iterable expression is evaluated in the enclosing scope; inner iterables are evaluated inside.
x = 10
squares = [x**2 for x in range(5)]  # x in comprehension is local
print(x)      # 10 — unchanged (Python 3 behavior)

5.4 range Object

  • range(stop), range(start, stop), range(start, stop, step).
  • range objects are lazy — they do not create a list.
  • Support len(), indexing, slicing, membership test (in), and reversed().
  • Step may be negative; step of 0 raises ValueError.

6. Defined vs Undefined Behavior

6.1 Defined

  • StopIteration signals iterator exhaustion; it is caught by for loops and generator machinery.
  • Loop variable binds the last value after normal loop completion.
  • else on a loop runs if and only if break was not executed.
  • continue does not affect else.
  • break inside try/finally still executes finally.

6.2 Undefined / Implementation-Defined

  • Mutating a list during iteration: CPython iterates by integer index. Adding elements may repeat them; removing elements may skip them. The spec does not define behavior for mutation during iteration.
  • Dictionary iteration order: Since Python 3.7, dict preserves insertion order (CPython implementation detail promoted to spec). Mutating a dict during iteration raises RuntimeError in CPython 3.3+.
  • Generator StopIteration propagation: Since Python 3.7 (PEP 479), StopIteration raised inside a generator is converted to RuntimeError. This was a breaking change.

7. Edge Cases from the Spec (CPython-Specific Notes)

7.1 Loop Variable Persists After Loop

for i in range(5):
    pass
print(i)   # 4 — i is bound to last value

for x in []:
    pass
# print(x)   # NameError if x was not previously defined — loop body never ran

7.2 Modifying a List During Iteration

data = [1, 2, 3, 4, 5]
for item in data:
    if item == 2:
        data.remove(item)   # DANGER: may skip element 3
print(data)   # [1, 3, 4, 5]  — but skipped 3 during iteration

# Safe approach: iterate over a copy
for item in data[:]:
    ...

7.3 else Clause for Search Pattern

def find_prime(lst):
    for n in lst:
        for d in range(2, n):
            if n % d == 0:
                break
        else:
            return n   # only reached if no 'break' in inner loop
    return None

print(find_prime([4, 6, 7, 10]))   # 7

7.4 StopIteration in Generators (PEP 479)

# Python 3.7+: StopIteration inside generator becomes RuntimeError
def bad_gen():
    yield 1
    raise StopIteration   # This becomes RuntimeError in Python 3.7+

import sys
gen = bad_gen()
next(gen)  # 1
try:
    next(gen)
except RuntimeError as e:
    print(f"RuntimeError: {e}")

7.5 Generator Lazy Evaluation

def counter(start=0):
    n = start
    while True:
        yield n
        n += 1

gen = counter()
print(next(gen))   # 0
print(next(gen))   # 1
print(next(gen))   # 2

7.6 Comprehension vs Loop Variable Scope

# Python 3: comprehension variable is local
result = [i * 2 for i in range(5)]
# 'i' is NOT accessible here (NameError if not previously defined)

# Python 2 LEGACY (not Python 3): was accessible — known as "variable leakage"

7.7 break with finally

for i in range(3):
    try:
        if i == 1:
            break
    finally:
        print(f"finally: {i}")
# Output:
# finally: 0
# finally: 1   <- finally still runs even when break exits loop

8. Version History (PEPs and Python Versions)

Feature PEP Version
for/while/break/continue Python 1.0
else clause on loops Python 1.0
Generator functions (yield) PEP 255 Python 2.2
Generator expressions PEP 289 Python 2.4
Comprehension scoping (local variables) Python 3.0
yield from PEP 380 Python 3.3
async for PEP 492 Python 3.5
StopIterationRuntimeError in generators PEP 479 Python 3.7
dict insertion-order guarantee (in spec) Python 3.7
walrus in comprehensions PEP 572 Python 3.8
zip(..., strict=True) PEP 618 Python 3.10
itertools.pairwise Python 3.10
itertools.batched Python 3.12

9. Implementation-Specific Behavior

9.1 CPython for Loop Bytecode

  • CPython generates GET_ITER (calls __iter__) then FOR_ITER (calls __next__ and jumps on StopIteration).
  • FOR_ITER is a tightly optimized bytecode instruction.
  • Internal list iteration uses listiterobject which accesses the underlying array directly (no Python-level __next__ call overhead).

9.2 CPython range Optimization

  • range objects have a C-level iterator that generates values on demand without Python object allocation per iteration.
  • for i in range(n) is significantly faster than for i in list(range(n)).

9.3 Generator Frame Suspension

  • CPython stores generator state in a frame object; yield suspends the frame.
  • Each next() call resumes the frame from the last yield point.
  • Generator frames consume memory until the generator is exhausted or garbage collected.

9.4 PyPy

  • JIT-compiled loops achieve near-C speed for numeric-heavy loops.
  • Loop unrolling and trace compilation are performed automatically.
  • range iteration benefits most from JIT warm-up.

10. Spec Compliance Checklist

  • for loop does not modify the iterable during iteration
  • break and continue only inside loops (no SyntaxError)
  • else on loops understood: runs only when no break
  • Iterator protocol: __iter__ returns self, __next__ raises StopIteration
  • Comprehension variables understood as local (Python 3)
  • StopIteration in generators raises RuntimeError (Python 3.7+, PEP 479)
  • dict iteration considers that mutations raise RuntimeError
  • finally runs even when break exits a try block inside a loop
  • Loop variable is accessible after loop (holds last value)
  • async for used only inside async def functions

11. Official Examples (Runnable Python 3.10+)

# ----------------------------------------------------------------
# 1. Basic for loop with range
# ----------------------------------------------------------------
for i in range(5):
    print(i, end=" ")   # 0 1 2 3 4
print()


# ----------------------------------------------------------------
# 2. for loop over sequence
# ----------------------------------------------------------------
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)


# ----------------------------------------------------------------
# 3. for loop with enumerate
# ----------------------------------------------------------------
for idx, fruit in enumerate(fruits, start=1):
    print(f"{idx}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry


# ----------------------------------------------------------------
# 4. while loop
# ----------------------------------------------------------------
n = 1
while n < 32:
    n *= 2
print(n)   # 32


# ----------------------------------------------------------------
# 5. while with else
# ----------------------------------------------------------------
x = 10
while x > 0:
    x -= 3
else:
    print(f"Loop done, x = {x}")   # x = -2


# ----------------------------------------------------------------
# 6. break — exit early
# ----------------------------------------------------------------
for i in range(10):
    if i == 5:
        break
    print(i, end=" ")   # 0 1 2 3 4
print()


# ----------------------------------------------------------------
# 7. continue — skip iteration
# ----------------------------------------------------------------
for i in range(10):
    if i % 2 == 0:
        continue
    print(i, end=" ")   # 1 3 5 7 9
print()


# ----------------------------------------------------------------
# 8. for...else search pattern
# ----------------------------------------------------------------
def find_in(lst, target):
    for item in lst:
        if item == target:
            print(f"Found: {target}")
            break
    else:
        print(f"Not found: {target}")

find_in([1, 2, 3], 2)    # Found: 2
find_in([1, 2, 3], 9)    # Not found: 9


# ----------------------------------------------------------------
# 9. Nested loops with break (only inner loop)
# ----------------------------------------------------------------
for i in range(3):
    for j in range(3):
        if j == 1:
            break
    print(f"i={i}, j={j}")   # j=1 for each i


# ----------------------------------------------------------------
# 10. Generator function
# ----------------------------------------------------------------
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
print([next(fib) for _ in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]


# ----------------------------------------------------------------
# 11. Comprehensions (all create local scope for variables)
# ----------------------------------------------------------------
squares = [x**2 for x in range(10)]
evens   = [x for x in range(20) if x % 2 == 0]
pairs   = [(x, y) for x in range(3) for y in range(3) if x != y]
sq_set  = {x**2 for x in range(-5, 6)}
sq_dict = {x: x**2 for x in range(5)}

print(squares[:5])   # [0, 1, 4, 9, 16]
print(sq_dict)       # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}


# ----------------------------------------------------------------
# 12. zip (Python 3.10+ strict=True)
# ----------------------------------------------------------------
names = ["Alice", "Bob", "Carol"]
scores = [95, 87, 92]
for name, score in zip(names, scores, strict=True):
    print(f"{name}: {score}")


# ----------------------------------------------------------------
# 13. zip and enumerate together
# ----------------------------------------------------------------
pairs_indexed = list(enumerate(zip(names, scores), start=1))
print(pairs_indexed)
# [(1, ('Alice', 95)), (2, ('Bob', 87)), (3, ('Carol', 92))]


# ----------------------------------------------------------------
# 14. iter() with sentinel (two-argument form)
# ----------------------------------------------------------------
import io
stream = io.StringIO("line1\nline2\nSTOP\nline3\n")
for line in iter(stream.readline, "STOP\n"):
    print(repr(line))   # 'line1\n', 'line2\n'


# ----------------------------------------------------------------
# 15. walrus in while loop
# ----------------------------------------------------------------
import re
text = "abc123def456ghi789"
pos = 0
while m := re.search(r"\d+", text[pos:]):
    print(m.group())
    pos += m.end()
# 123
# 456
# 789

Section Topic URL
§8.2 while statement https://docs.python.org/3/reference/compound_stmts.html#the-while-statement
§8.3 for statement https://docs.python.org/3/reference/compound_stmts.html#the-for-statement
§7.9 break statement https://docs.python.org/3/reference/simple_stmts.html#the-break-statement
§7.10 continue statement https://docs.python.org/3/reference/simple_stmts.html#the-continue-statement
§3.4.2 __iter__ protocol https://docs.python.org/3/reference/datamodel.html#object.iter
§6.2.8 Generator expressions https://docs.python.org/3/reference/expressions.html#generator-expressions
§6.2.9 Yield expressions https://docs.python.org/3/reference/expressions.html#yield-expressions
range Built-in range object https://docs.python.org/3/library/stdtypes.html#range
itertools Iterator building blocks https://docs.python.org/3/library/itertools.html
PEP 255 Generator functions https://peps.python.org/pep-0255/
PEP 289 Generator expressions https://peps.python.org/pep-0289/
PEP 479 StopIteration in generators https://peps.python.org/pep-0479/
PEP 492 Async for / async with https://peps.python.org/pep-0492/
PEP 572 Walrus operator https://peps.python.org/pep-0572/