Question: I generally understand the concept of async vs threads/processes, I am just a bit confused when I am reading about the event loop of asyncio. When you use asyncio.run() I presume it creates an event loop? Does this event loop use an executor? The link above says the event loop will use the default executor, which after a brief google search it appears to be ThreadPoolExecutor. I am a bit confused if I am writing coroutines why would that use ThreadPoolExecutor since I don't want to execute my async code in threads.

Answer:  When you use asyncio.run() I presume it creates an event loop?  Yes, from https://docs.python.org/3/library/asyncio-runner.html#asyncio.run:  This function always creates a new event loop and closes it at the end.    Does this event loop use an executor?  The event loop provides single-threaded concurrency for IO-bound work. For CPU-bound work, which would otherwise block the event loop, asyncio lets you execute code outside the event loop in a separate thread or process, by using an executor. This is done by calling run_in_executor. It wraps the executor API and returns an awaitable which lets you use it transparently in async/await code, whereas if you used an executor directly, you would have to deal with futures and callbacks.1 The event loop has a default executor which will be used by run_in_executor if no other executor is provided as argument. A custom default executor can be provided by calling set_default_executor. If you don't call run_in_executor yourself, the code you pass to asyncio.run will not be executed in an executor, but asyncio might still use the default executor internally for some of its own code, I guess. asyncio.run will automatically shut down the default executor at the end by calling shutdown_default_executor internally (since Python 3.9).2  1 To illustrate the differences, suppose you want to do some CPU-intensive calculation in a separate thread and then report on the result. Using ThreadPoolExecutor and Future.add_done_callback: from concurrent.futures import ThreadPoolExecutor from time import sleep def calculate(): print("Calculating...") sleep(1) return 42 def report(future): print(f"The result is {future.result()}") with ThreadPoolExecutor() as executor: future = executor.submit(calculate) future.add_done_callback(report)  Using ThreadPoolExecutor and concurrent.futures.as_completed: from concurrent.futures import ThreadPoolExecutor, as_completed from time import sleep def calculate(): print("Calculating...") sleep(1) return 42 with ThreadPoolExecutor() as executor: futures = [executor.submit(calculate)] for future in as_completed(futures): print(f"The result is {future.result()}")  Using asyncio: import asyncio from time import sleep def calculate(): print("Calculating...") sleep(1) return 42 async def main(): loop = asyncio.get_event_loop() result = await loop.run_in_executor(None, calculate) print(f"The result is {result}") asyncio.run(main())   2 I'm not sure if the remark about the change in Python 3.9 in https://docs.python.org/3/library/asyncio-runner.html#asyncio.run means that in earlier versions it did not shut down the executor at all, or if it did it by other means than calling shutdown_default_executor:  This function runs the passed coroutine, taking care of managing [...], and closing the threadpool. [...] Changed in version 3.9: Updated to use loop.shutdown_default_executor().