Skip to content

SCR120: dangerous-loop-closure

Function defined in a loop uses the loop's variable

Code SCR120
Name dangerous-loop-closure
Category Scripts (SCR)
Default severity warning
Enabled by default no
Fix available no
ign-lint equivalent none

Reports names read by a lambda or nested def created inside a for loop when the name is a variable the loop rebinds (its target, a name assigned in its body or the target of an except clause in it), and names read by a lambda created in a comprehension when the name is one of its variables (a list comprehension binds them in the enclosing scope, the other kinds in their own; either way they are rebound on every iteration).

Generator expressions created in the loop count too: one reads the loop's variables when it is consumed (in its body and its conditions; its first iterable is evaluated where it is written).

Not reported: a function that cannot outlive the iteration that creates it, so that it reads that iteration's value: a lambda called where it is written ((lambda: u)()), a lambda passed directly to the builtin sorted, min, max, map, filter or reduce as an argument the builtin calls while it runs (the cmp and key of sorted, the key of min and max, the first argument of map, filter and reduce: [sorted(rows, key=lambda r: r[c]) for c in cols]; any other argument is a value the builtin may return, max(lambda: t, lambda: t) returns a lambda), and a def (or a name bound to a lambda) whose every use is a call later in the same block of the loop body, in the same scope. A use before the definition, any other use (stored, passed, returned), a decorator, a call from a nested function or a closure it returns still count. So does a generator: calling a generator function (a def or lambda with yield) runs none of its body, and a generator expression runs when it is consumed, unless it is consumed where it is written: as the iterable of a builtin that consumes it (sum, any, all, list, tuple, set, dict, sorted, a single argument of min or max (not with *args that may add another: max(g, *rest) may return g; the items of a literal *args are arguments at their positions, max(*(g,)) consumes g), the sequences of map, filter, reduce and zip, ''.join), of a for statement or of a list, set or dict comprehension. A generator function whose every call is consumed that way (total += sum(h())) is not reported either.

The function reads the variable when it is called, not when it is created. Functions called after the loop (callbacks given to system.util.invokeLater, handlers stored in a dict) all see the last value.

for path in paths:
system.util.invokeLater(lambda: system.tag.writeBlocking([path], [0]))

Use instead:

for path in paths:
system.util.invokeLater(lambda path=path: system.tag.writeBlocking([path], [0]))

The same in a comprehension (Jython: [2, 2, 2]):

handlers = [lambda: k for k in range(3)]

Corresponds to W0640 cell-var-from-loop.


Generated from the rule metadata by iglint generate-docs; iglint rule SCR120 prints the same page in the terminal. Source: docs/rules/SCR120.md.