This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Unexpected exception handler behavior in Jupyter when returning task objects created with create_task
Type: behavior Stage: resolved
Components: asyncio Versions: Python 3.8
process
Status: closed Resolution: rejected
Dependencies: Superseder:
Assigned To: Nosy List: aeros, asvetlov, jeanmonet, yselivanov
Priority: normal Keywords:

Created on 2020-05-03 17:36 by jeanmonet, last changed 2022-04-11 14:59 by admin. This issue is now closed.

Messages (4)
msg367988 - (view) Author: John Smith (jeanmonet) Date: 2020-05-03 17:36
Hi,

I'm running the following code in a Jupyter notebook (so there is already a running loop).

`run_coro` (non-async function) adds given coroutines to the loop with `loop.create_task`. I want to return the task objects created by `loop.create_task` to check for result later in the code.

Adding the task objects to a list and returning the list has the exception handler behave differently than expected.


```
async def test1():
    ''' Enumerate numbers from 1 to 9'''
    n = 0
    while (n := n + 1) < 10:
        print(f'Test 1: {n}')
        await asyncio.sleep(0.3)
    return 'Test 1: DONE'


async def test2():
    ''' Enumerate numbers starting 1, raising exception at 5'''
    n = 0
    while (n := n + 1) < 10:
        if n == 5:
            raise ValueError('Test Exception')
        print(f'Test 2: {n}')
        await asyncio.sleep(0.2)


def handle_exception(loop, context):
    if context['exception']:
        print(f'Caught exception <{context["exception"]}> while executing {context["future"]}')


def run_coro(loop, coros):
    loop.set_exception_handler(handle_exception)
    tasks = deque()
    for coro in coros:
        task = loop.create_task(coro)
        #tasks.append(task)
        # ^--- if I uncomment, exc handler behaves strangely
    return tasks
    # ^-- BUT if I remove the "return" statement, it behaves correctly again


loop = asyncio.get_running_loop()   # retrieves Jupyter's running loop

tasks = run_coro(loop, [test1(), test2()])
```

Normal (expected output):

```
Test 1: 1
Test 2: 1
Test 2: 2
Test 1: 2
Test 2: 3
Test 1: 3
Test 2: 4
Caught exception <Test Exception> while executing <Task finished name='Task-214' coro=<test2() done, defined at <ipython-input-225-b5df8e07130e>:10> exception=ValueError('Test Exception')>
Test 1: 4
Test 1: 5
Test 1: 6
Test 1: 7
Test 1: 8
Test 1: 9
```

If I uncomment `tasks.append(task)` and return the list of task objects, output is:

```
Test 1: 1
Test 2: 1
Test 2: 2
Test 1: 2
Test 2: 3
Test 1: 3
Test 2: 4
Test 1: 4
Test 1: 5
Test 1: 6
Test 1: 7
Test 1: 8
Test 1: 9
```

If I re-run the code (re-run the Jupyter cell) the exception message would appear at the beginning of the output (as if it had been waiting in a queue somewhere), so for some reason it seems to get eaten-up somewhere.

The code works normally again if I remove the return statement from the `run_coro` function.

Is this expected behaviour? Am I missing something?
msg367989 - (view) Author: John Smith (jeanmonet) Date: 2020-05-03 18:12
Additional note:

In a almost identical set-up, the simple fact of assigning the task object to a variable:

`task = loop.create_task(coroutine())`

instead of just calling:

`loop.create_task(coroutine())`

...results in the same unexpected behavior in exception handling, without even attempting to return the task object.
msg368003 - (view) Author: Kyle Stanley (aeros) * (Python committer) Date: 2020-05-04 00:03
John Smith wrote:
> The code works normally again if I remove the return statement from the `run_coro` function.

I am a bit surprised that the code works as intended simply by removing the return statement. Perhaps Yury or Andrew can clarify on that point.

From looking at the code above, the main issue seems to be that the tasks are not awaited or cancelled and cleaned up at any point. When creating a task without ever awaiting it, there's no guarantee that it will be completed within the duration of the event loop. This is handled for the user when using asyncio.run() by cancelling the tasks and propagating any exceptions during event loop finalization, but should be done manually or by the event loop when not using it. For an example, see _cancel_all_tasks() in https://github.com/python/cpython/blob/d699d5e6178adca785a8701c32daf5e18fad0bf1/Lib/asyncio/runners.py#L54.

So, my best guess as to what's happening is that the tasks are still in progress while the event loop is finalizing, and the exception handler isn't called. But, I can't say that for sure without knowing the implementation of the Jupyter event loop.

For now, my recommended solution would be to include something like the above _cancel_all_tasks() or simply await your tasks at the end to ensure they are completed before the event loop is finalized. Alternatively, Jupyter could include something like that in their event loop finalization process, so that users don't have to include it on their own when using asyncio.

I would also consider using asyncio.run(), but I'm not certain if it works correctly in a Jupyter notebook. I'm aware that it's not always a viable option when it is desired to use an existing event loop instead of creating a separate one; that's why I'm not explicitly recommending it for this situation.
msg415007 - (view) Author: Andrew Svetlov (asvetlov) * (Python committer) Date: 2022-03-12 19:39
tasks are not awaited, this is the problem.
The reproducer is not correct.
Closing.
History
Date User Action Args
2022-04-11 14:59:30adminsetgithub: 84667
2022-03-12 19:39:37asvetlovsetstatus: open -> closed
resolution: rejected
messages: + msg415007

stage: resolved
2020-05-04 00:03:31aerossetnosy: + aeros
messages: + msg368003
2020-05-03 18:12:21jeanmonetsetmessages: + msg367989
2020-05-03 17:37:33jeanmonetsettype: behavior
2020-05-03 17:36:41jeanmonetcreate