title: How to correctly handle cancelled tasks in Python's `asyncio.gather`
body: <p>So I'm taking another stab at the <code>asyncio</code> module now that 3.8 is out. However, I am getting unexpected results when trying to do a graceful shutdown of the event loop. Specifically I am listening for a <code>SIGINT</code>, cancelling the running <code>Task</code>s, gathering those <code>Task</code>s, and then <code>.stop()</code>ing the event loop. I know that <code>Task</code>s raise a <code>CancelledError</code> when they are cancelled which will propagate up and end my call to <code>asyncio.gather</code> unless, according to the <a href="https://docs.python.org/3/library/asyncio-task.html#running-tasks-concurrently" rel="noreferrer">documentation</a>, I pass <code>return_exceptions=True</code> to <code>asyncio.gather</code>, which should cause <code>gather</code> to wait for all the <code>Task</code>s to cancel and return an array of <code>CancelledError</code>s. However, it appears that <code>return_exceptions=True</code> still results in an immediate interruption of my <code>gather</code> call if I try to <code>gather</code> cancelled <code>Task</code>s.</p>
  
  <p>Here is the code to reproduce the effect. I am running python 3.8.0:</p>
  
  <pre class="lang-py prettyprint-override"><code># demo.py
  
  import asyncio
  import random
  import signal
  
  
  async def worker():
      sleep_time = random.random() * 3
      await asyncio.sleep(sleep_time)
      print(f"Slept for {sleep_time} seconds")
  
  async def dispatcher(queue):
      while True:
          await queue.get()
          asyncio.create_task(worker())
          tasks = asyncio.all_tasks()
          print(f"Running Tasks: {len(tasks)}")
  
  async def shutdown(loop):
      tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
      for task in tasks:
          task.cancel()
      print(f"Cancelling {len(tasks)} outstanding tasks")
      results = await asyncio.gather(*tasks, return_exceptions=True)
      print(f"results: {results}")
      loop.stop()
  
  async def main():
      loop = asyncio.get_event_loop()
      loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(loop)))
      queue = asyncio.Queue()
      asyncio.create_task(dispatcher(queue))
  
      while True:
          await queue.put('tick')
          await asyncio.sleep(1)
  
  
  asyncio.run(main())
  </code></pre>
  
  <p>Output:</p>
  
  <pre class="lang-sh prettyprint-override"><code>&gt;&gt; python demo.py 
  Running Tasks: 3
  Slept for 0.3071352174511871 seconds
  Running Tasks: 3
  Running Tasks: 4
  Slept for 0.4152310498820644 seconds
  Running Tasks: 4
  ^CCancelling 4 outstanding tasks
  Traceback (most recent call last):
    File "demo.py", line 38, in &lt;module&gt;
      asyncio.run(main())
    File "/Users/max.taggart/.pyenv/versions/3.8.0/lib/python3.8/asyncio/runners.py", line 43, in run
      return loop.run_until_complete(main)
    File "/Users/max.taggart/.pyenv/versions/3.8.0/lib/python3.8/asyncio/base_events.py", line 608, in run_until_complete
      return future.result()
  asyncio.exceptions.CancelledError
  </code></pre>
  
  <p>I'm guessing there is still something about the event loop that I don't understand, but I would expect all the <code>CancelledError</code>s to come back as an array of objects stored in <code>results</code> and then be able to continue on rather than to see an error immediately. </p>