Nested functions and lambdas can reference variables defined in enclosing scopes. This can create tricky bugs when the variable and the function are defined in a loop. If the function is called in another iteration or after the loop finishes, it will see the variables' last value instead of seeing the values corresponding to the iteration where the function was defined.


Capturing loop variables might work for some time but:

* it makes the code difficult to understand.
* it increases the risk of introducing a bug when the code is refactored or when dependencies are updated. See an example with the builtin "map" below.

One solution is to add a parameter to the function/lambda and use the previously captured variable as its default value. Default values are only executed once, when the function is defined, which means that the parameter's value will remain the same even when the variable is reassigned in following iterations.


Another solution is to pass the variable as an argument to the function/lambda when it is called.


This rule raises an issue when a function or lambda references a variable defined in an enclosing loop.

== Noncompliant Code Example

----
def run():
    mylist = []
    for i in range(5):
        mylist.append(lambda: i)  # Noncompliant

        def func():
            return i  # Noncompliant
        mylist.append(func)

def example_of_api_change():
    """"
    Passing loop variable as default values also makes sure that the code is future-proof.
    For example the following code will work as intended with python 2 but not python 3.
    Why? because "map" behavior changed. It now returns an iterator and only executes
    the lambda when required. The same is true for other functions such as "filter".
    """
    lst = []
    for i in range(5):
        lst.append(map(lambda x: x + i, range(3)))  # Noncompliant
    for sublist in lst:
        # prints [4, 5, 6] x 4 with python 3, with python 2 it prints [0, 1, 2], [1, 2, 3], ...
        print(list(sublist))
----

== Compliant Solution

----
def run():
    mylist = []
    for i in range(5):
        mylist.append(lambda i=i: i)  # passing the variable as a parameter with a default value

        def func(i=i):  # same for nested functions
            return i
        mylist.append(func)

def example_of_api_change():
    """"
    This will work for both python 2 and python 3.
    """
    lst = []
    for i in range(5):
        lst.append(map(lambda x, value=i: x + value, range(3)))  # Passing "i" as a default value
    for sublist in lst:
        print(list(sublist))
----

== Exceptions

No issue will be raised if the function or lambda is directly called in the same loop. This still makes the design difficult to understand but it is less error prone.


----
def function_called_in_loop():
    for i in range(10):
        print((lambda param: param * i)(42)) # Calling the lambda directly

        def func(param):
            return param * i

        print(func(42))  # Calling "func" directly
----

== See

* https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments[The Hitchhiker's Guide to Python - Common Gotchas]
* Python documentation - https://docs.python.org/3/reference/compound_stmts.html#function-definitions[Function definitions]

ifdef::env-github,rspecator-view[]
'''
== Comments And Links
(visible only on this page)

include::../comments-and-links.adoc[]
endif::env-github,rspecator-view[]
