Why async function binds name incorrectly from the outer scope?

Question:

Here is an async function generator by iterating a for loop. I expected this closure to respect the names from the outer scope.

import asyncio

coroutines = []
for param in (1, 3, 5, 7, 9):

    async def coro():
        print(param ** 2)
        # await some_other_function()
    
    coroutines.append(coro)


# The below code is not async, I made a poor mistake.
# Michael Szczesny's answer fixes this.
for coro in coroutines:
    asyncio.run(coro())

While I was expecting the results to be 1, 9, 25, 49, 81 after running, here is the actual output:

81
81
81
81
81

Unexpectedly here, the name param has taken the same value each time.

Can you explain why this happens and how I can achieve the task of creating lots of async functions in a for loop with names binding correctly?

Edit: Here is what my program looks like in A LOT MORE detail even though the actual problem is the above. Here you’ll see that why coro function shouldn’t take param as a parameter in my case.

import asyncio
from dataclasses import dataclass
from typing import Any

# Using dataclass to make IDE give auto-completion suggestions.

@dataclass
class Params:
    first: Any
    second: Any
    third: Any
    fourth: Any
    fifth: Any


class MyClass:
    def __init__(self):
        coroutines = []
        for param in (1, 3, 5, 7, 9):

            async def coro():
                await asyncio.sleep(1)
                print(param ** 2)

            coroutines.append(coro)
        self.run = Params(*coroutines)


async def main():
    obj = MyClass()

    await asyncio.gather(
        obj.run.first(),  # each of them should be uniquely defined...
        obj.run.second(),  # ...by `param` in the `for` loop.
        obj.run.third(),
        obj.run.fourth(),
        obj.run.fifth()
    )

asyncio.run(main())

Output:

81
81
81
81
81
Asked By: UpTheIrons

||

Answers:

Only once the coro function is run, will it evaluate the line:

print(param ** 2)

At that time, it will check the value of param and evaluate it. So what is the value of param then?

Looking at the code, we can see that the param value is set during iteration:

for param in (1, 3, 5, 7, 9):
    ...

# For loop is over, param is now equal to 9
# since it was the last value it iterated on

Then only afterwards is the coro function run:

for coro in coroutines:
    asyncio.run(coro())  # 'param' is still set to 9, meaning that the output will always be 81

You could instead add a parameter to the coro function:

async def coro(coro_param):
    print(coro_param ** 2)

… and pass the parameter to the function when it is called:

for param in (1, 3, 5, 7, 9):
    asyncio.run(coro(param))
Answered By: Xiddoc

The included code does not run asynchronously or uses coro as a closure. Functions are evaluated at runtime in python. An asynchronous solution would look like this

import asyncio

def create_task(param):
    async def coro():
        await asyncio.sleep(1) # make async execution noticeable
        print(param ** 2)
    return coro                # return closure capturing param

async def main():
    await asyncio.gather(*[create_task(param)() for param in (1,3,5,7,9)])

asyncio.run(main())

Output after 1 second

1
9
25
49
81
Answered By: Michael Szczesny