Fundamentals 11 min read

Why Python’s for‑loop Variables Leak Outside the Loop (and What It Means)

This article explores why Python’s for‑loop index variables remain visible after the loop, examines the underlying bytecode and AST handling, and explains why this behavior persists and can be useful in real‑world code.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Why Python’s for‑loop Variables Leak Outside the Loop (and What It Means)

We begin with a test function that mistakenly uses the loop variable i in the second loop instead of t , illustrating how the sum‑and‑product expectation can be misleading.

<code>def foo(lst):
    a = 0
    for i in lst:
        a += i
    b = 1
    for t in lst:
        b *= i  # bug: should use t
    return a, b
</code>

Python’s for‑loop target (the index variable) leaks into the surrounding function scope, which many developers find surprising. For example:

<code>for i in [1, 2, 3]:
    pass
print(i)  # prints 3
</code>

The language specification explicitly states that the loop variable remains bound after the loop finishes, unless the iterable is empty, in which case a NameError is raised:

<code>for i in []:
    pass
print(i)  # NameError
</code>

This behavior stems from Python’s simple and elegant scope rules: the innermost scope that can contain a variable is the function body, not the loop block. The compiler treats the loop variable like any other local variable, generating LOAD_FAST and STORE_FAST bytecode instructions.

<code>0 LOAD_CONST        1 (0)
3 STORE_FAST        1 (a)
6 SETUP_LOOP       24 (to 33)
9 LOAD_FAST        0 (lst)
12 GET_ITER
13 FOR_ITER        16 (to 32)
16 STORE_FAST        2 (i)
19 LOAD_FAST        1 (a)
22 LOAD_FAST        2 (i)
25 INPLACE_ADD
26 STORE_FAST        1 (a)
29 JUMP_ABSOLUTE      13
32 POP_BLOCK
33 LOAD_FAST        1 (a)
36 RETURN_VALUE
</code>

During compilation, the AST node for a for statement creates a Name node for the target variable with a Store context. The symbol‑table visitor marks this as DEF_LOCAL , so the variable is handled exactly like any other local name.

<code>case For_kind:
    VISIT(st, expr, s->v.For.target);
    VISIT(st, expr, s->v.For.iter);
    VISIT_SEQ(st, stmt, s->v.For.body);
    if (s->v.For.orelse)
        VISIT_SEQ(st, stmt, s->v.For.orelse);
    break;
</code>

The same logic applies to list comprehensions (pre‑Python‑3) and other constructs where the loop variable is introduced.

Because this feature is useful—e.g., counting loop iterations with enumerate —and widely used in real code, it is unlikely to be removed:

<code>for i, item in enumerate(somegenerator()):
    dostuffwith(i, item)
print('The loop executed {0} times!'.format(i+1))
</code>

Understanding the underlying implementation helps developers write correct code and avoid subtle bugs, such as the classic lambda‑capture pitfall:

<code>def foo():
    lst = []
    for i in range(4):
        lst.append(lambda: i)
    print([f() for f in lst])  # prints [3, 3, 3, 3]
</code>

In summary, Python’s design choice to keep loop variables in the function scope simplifies the language’s semantics, preserves backward compatibility, and provides practical benefits that many developers rely on.

PythonASTbytecodeLanguage Designvariable scopefor loop
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.