2

This is example of cancelling task:

import asyncio


async def some_func():
    await asyncio.sleep(2)
    print('Haha! Task keeps running!')
    await asyncio.sleep(2)

async def cancel(task):
    await asyncio.sleep(1)
    task.cancel()

async def main():
    func_task = asyncio.ensure_future(some_func())
    cancel_task = asyncio.ensure_future(cancel(func_task))
    try:
        await func_task
    except asyncio.CancelledError:
        print('Task cancelled as expected')

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

# Task cancelled as expected
# [Finished in 1.2s]

It works ok, task was cancelled. If CancelledError caught inside some_func task wouldn't be cancelled:

async def some_func():
    try:
        await asyncio.sleep(2)
    except:
        pass
    print('Haha! Task keeps running!')
    await asyncio.sleep(2)

# Haha! Task keeps running!
# [Finished in 3.2s]

It can be easy to forgot I shouldn't suppress exceptions anywhere inside async code (or some_func can be third party code, for example), but task should be cancelled. Is there anyway I can do that? Or ignored CancelledError means task can't be cancelled at all?

Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159

2 Answers2

5

You cannot cancel task that suppresses CancelledError. This is similar to impossibility to close generator which ignores GeneratorExit.

This is intentional behavior. Task may want to do some extra work (e.g. resource cleanup) on cancelling, thus catching CancelledError may be good idea but suppressing usually is sign of programming error.

Python usually allows you to shoot own feet if you have uncompromising intention to do this.

Catching all exceptions even forbids closing python process by pressing <Ctrl+C> because it's translated into KeyboardInterrupt internally.

Peter Hansen
  • 21,046
  • 5
  • 50
  • 72
Andrew Svetlov
  • 16,730
  • 8
  • 66
  • 69
2

The real problem here is using a blanket "except" clause, which is either almost never a good idea or, in the opinion of some, absolutely never a good idea.

Rather than using "except:" by itself, always specify an exception or base exception class which you want to catch. Note that as of Python 3.8, CancelledError is a subclass of BaseException rather than Exception, while all exceptions you would normally want to catch with a "except:" are subclasses of Exception (which is itself a child class of BaseException). Here are docs on the exception hierarchy showing this relationship.

So this is better:

try:
    ... (your code here)
except Exception:
    pass

This will catch and silently ignore all normal exceptions (which is still undesirable in most real code but definitely valid in some), while allowing others to go past. Note that CancelledError, SystemExit, KeyboardInterrupt and a couple of others will still get through. If you don't like the "noise" caused by those on exit, then you should catch those specifically at (most likely) a higher level.

If you truly do want to swallow literally everything except CancelledError, then you would do this instead:

try:
    ... (your code here)
except asyncio.CancelledError:
    raise     # avoid swallowing this one exception
except BaseException:
    pass      # silently swallow everything else: usually not a good idea
Peter Hansen
  • 21,046
  • 5
  • 50
  • 72