Skip to content

Soonify - Return Values

In the last chapter, I showed you how to use asyncer.soonify() to run multiple async functions concurrently.

We wrote some code to call some async functions with some arguments soon, concurrently.

But what happens if you want to retrieve the return value from those async functions after they are run?

I'll show you how to do that here. 🤓

Function Not Returning

Let's see the last example do_work() async function we used in the previous chapter:

# Code above omitted 👆

async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")


async def get_data():
    async with asyncer.create_task_group() as task_group:
        task_group.soonify(do_work)(name="Yury")
        task_group.soonify(do_work)(name="Nathaniel")
        task_group.soonify(do_work)(name="Alex")


async def main():
    await get_data()


anyio.run(main)

This function takes a parameter name and then it prints a message.

But it never returns anything (which is equivalent to returning None).

Return a Value

But now let's say that we don't really want that function to print the message directly, just to return the string.

Maybe because we could want to do something else later with the value, or for any other reason:

# Code above omitted 👆

async def do_work(name: str):
    await anyio.sleep(1)
    message = f"Hello, {name}"
    return message

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    message = f"Hello, {name}"
    return message


async def get_data():
    async with asyncer.create_task_group() as task_group:
        soon_value1 = task_group.soonify(do_work)(name="Yury")
        soon_value2 = task_group.soonify(do_work)(name="Nathaniel")
        soon_value3 = task_group.soonify(do_work)(name="Alex")

    data = [soon_value1.value, soon_value2.value, soon_value3.value]
    return data


async def main():
    data = await get_data()
    for message in data:
        print(message)


anyio.run(main)

Store SoonValue Objects

When you use:

task_group.soonify(async_function)(arg1, arg2)

...that call returns a special object (an instance of a class SoonValue) that you can store in a variable:

# Code above omitted 👆

async def do_work(name: str):
    await anyio.sleep(1)
    message = f"Hello, {name}"
    return message


async def get_data():
    async with asyncer.create_task_group() as task_group:
        soon_value1 = task_group.soonify(do_work)(name="Yury")
        soon_value2 = task_group.soonify(do_work)(name="Nathaniel")
        soon_value3 = task_group.soonify(do_work)(name="Alex")

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    message = f"Hello, {name}"
    return message


async def get_data():
    async with asyncer.create_task_group() as task_group:
        soon_value1 = task_group.soonify(do_work)(name="Yury")
        soon_value2 = task_group.soonify(do_work)(name="Nathaniel")
        soon_value3 = task_group.soonify(do_work)(name="Alex")

    data = [soon_value1.value, soon_value2.value, soon_value3.value]
    return data


async def main():
    data = await get_data()
    for message in data:
        print(message)


anyio.run(main)

Get the Return Value from SoonValue Objects

When one of these async functions started with soonify() finishes, the return value of the function is stored inside the SoonValue object, in the attribute soon_value1.value:

# Code above omitted 👆

async def get_data():
    async with asyncer.create_task_group() as task_group:
        soon_value1 = task_group.soonify(do_work)(name="Yury")
        soon_value2 = task_group.soonify(do_work)(name="Nathaniel")
        soon_value3 = task_group.soonify(do_work)(name="Alex")

    data = [soon_value1.value, soon_value2.value, soon_value3.value]
    return data

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    message = f"Hello, {name}"
    return message


async def get_data():
    async with asyncer.create_task_group() as task_group:
        soon_value1 = task_group.soonify(do_work)(name="Yury")
        soon_value2 = task_group.soonify(do_work)(name="Nathaniel")
        soon_value3 = task_group.soonify(do_work)(name="Alex")

    data = [soon_value1.value, soon_value2.value, soon_value3.value]
    return data


async def main():
    data = await get_data()
    for message in data:
        print(message)


anyio.run(main)

After the async with block, the task group will wait for all of the concurrent functions/tasks to finish before any code below is executed.

This means that after the async with block those functions will have already finished, and the SoonValue objects will contain the return value already.

Typing Support

Because of the way Asyncer is designed, you will get typing support in these SoonValue objects and their soon_value1.value attribute.

This means that your editor will know the type of that soon_value1.value, and will be able to provide you autocompletion:

And because the editor knows the types of the values, you will also get inline errors:

And because the editor can follow and infer this type information, you will also get type support down the line in anything that uses these values.

For example, it will be able to infer that get_data() returns a list of strings.

And when you await and access the return value of get_data() you will also get editor support:

Notice that you didn't even have to add the type to most of the variables, only to the function parameters, and everything else was inferred.

All this typing support also means that you can use tools like mypy to verify that your code is correct and prevent many bugs.

SoonValue Objects Inside the async with Block

If you try to access the soon_value1.value attribute of the SoonValue object inside the async with block, you will normally get an error:

# Code above omitted 👆

async def get_data():
    async with asyncer.create_task_group() as task_group:
        soon_value1 = task_group.soonify(do_work)(name="Yury")
        soon_value2 = task_group.soonify(do_work)(name="Nathaniel")
        soon_value3 = task_group.soonify(do_work)(name="Alex")
        print(soon_value1.value)

    data = [soon_value1.value, soon_value2.value, soon_value3.value]
    return data

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    message = f"Hello, {name}"
    return message


async def get_data():
    async with asyncer.create_task_group() as task_group:
        soon_value1 = task_group.soonify(do_work)(name="Yury")
        soon_value2 = task_group.soonify(do_work)(name="Nathaniel")
        soon_value3 = task_group.soonify(do_work)(name="Alex")
        print(soon_value1.value)

    data = [soon_value1.value, soon_value2.value, soon_value3.value]
    return data


async def main():
    data = await get_data()
    for message in data:
        print(message)


anyio.run(main)

That will raise an exception like this:

PendingValueException: The return value of this task is still pending. Maybe you forgot to access it after the async with asyncer.create_task_group() block. If you need to access values of async tasks inside the same task group, you probably need a different approach, for example with AnyIO Streams.

That print(soon_value1.value) is still inside the async with block for the task group. And by that point, none of those async functions have been run yet.

Remember that the end of the async with block acts as if it had an implicit await.

At the end of the async with block, Python will run any async code that is pending (in this case, the async functions you passed to soonify()). It will wait for that to finish before continuing below.

And when Python continues below that async with block, as all the async functions were already called and awaited, all the SoonValue objects will have their soon_value1.value attributes available.

But before the values are available, trying to access them will raise that exception.

Check When a SoonValue Object is ready

If there are some await points inside of the async with block, Python would go and run pending async code at those await points (including the async functions you passed to soonify()).

Because of that, the soon_value1.value attributes of the SoonValue objects could have their value available, even inside of the async with block.

You can check if the value is already available by using the soon_value.ready attribute, it will be True or False:

# Code above omitted 👆

async def get_data():
    async with asyncer.create_task_group() as task_group:
        soon_value1 = task_group.soonify(do_work)(name="Yury")
        await anyio.sleep(2)
        if soon_value1.ready:
            print(f"Preview value1: {soon_value1.value}")

        # Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    message = f"Hello, {name}"
    return message


async def get_data():
    async with asyncer.create_task_group() as task_group:
        soon_value1 = task_group.soonify(do_work)(name="Yury")
        await anyio.sleep(2)
        if soon_value1.ready:
            print(f"Preview value1: {soon_value1.value}")
        soon_value2 = task_group.soonify(do_work)(name="Nathaniel")
        soon_value3 = task_group.soonify(do_work)(name="Alex")

    data = [soon_value1.value, soon_value2.value, soon_value3.value]
    return data


async def main():
    data = await get_data()
    for message in data:
        print(message)


anyio.run(main)

Here we have an await inside the async with block. It sleeps for 2 seconds.

Because it's an await, it will tell Python to go and run any other pending async code.

So, Python will run (or at least start) the pending async function we passed to soonify().

The 2 seconds is more than what the function will take to finish because it waited for 1 second inside. So, after that await, the soon_value1.value attribute will be available.

We verify that first, checking that soon_value1.ready is True, and then we can safely print the value.

Tip

If you feel like you need to access values generated by the async functions inside the same async with block for a task group, you might need to use a different approach, for example, with AnyIO Streams.