I'm sorry but how do you jump from 1. Polling to 2. Asyncio
There's so many solutions in the middle, I have this theory that most people that get into async don't really know what threading is. Maybe they have a world vision where before 2023 python just could not do more than one thing at once, that's what the GIL was right? But now after 3.12 Guido really pulled himself by the bootstraps and removed the GIL and implemented async and now python can do more than one thing at a time so they start learning about async to be able to do more than one thing at a time.
This is a huge disconnect between what python devs are actually building, a different api towards concurrency. And some junior devs that think they are learning bleeding edge stuff when they are actually learning fundamentals through a very contrived lens.
It 100% comes from ex-node devs, I will save the node criticism, but node has a very specific concurrency model, and node devs that try out python sometimes run to asyncio as a way to soften the learning curve of the new language. And that's how they get into this mess.
The python devs are working on these features because they have to work on something, and updates to foundational tech are supposed to have effects in decades, it's very rare that you need to use bleeding edge features. In 95% of the cases, you should be restricting yourself to using features from versions that are 5-10 years old, especially if you come from other languages! You should start old to new, not new to old.
Sorry, for the rant, or if I misjudged, making a broader claim based on multiple perspectives.
I think the core mistake is conflating async with threading and with the GIL, which causes people to treat asyncio like a magic GIL remover. For reference asyncio landed in CPython with Python 3.4 in March 2014, while threads and the GIL predate that, so Python could do concurrent IO long before 2023. In my experience asyncio is cooperative IO concurrency, so you must yield with await or explicitly offload blocking CPU work to concurrent.futures.ProcessPoolExecutor or run_in_executor, and you protect shared state with asyncio.Queue or an asyncio.Lock rather than assuming tasks are isolated. I've found the most practical pattern is message passing for state and keeping CPU heavy work in a process pool, because chasing a blocked event loop in production is a miserable way to learn concurrency.
I think they were already in the async world and needed message passing -- the polling code was also in python async.
As of 3.14 running without the GIL is optional, but the default still has the GIL in place. 3.13 had it as experimental, but not officially supported. 3.12 and back are all GIL all day.
Python's asyncio library is single threaded, so I'm not sure why you are talking about threads and asyncio like they have anything to do with each other.
Python has been able to do more then one thing at a time for a long time. That's what the multiprocess library is for. It's not an ideal solution, but it does exist.