Not async in general, but asyncio in particular.
Before 3.7, you had to be able to fetch the loop, handle its reference, pass it around, manage its life cycle, etc.
Starting from 3.7, only low level code (E.G: framework authors) would need to, the API is organized so that by default, you always perform something in the current running loop.
It's a big deal, and remove a ton of complexity. Maintenance is easier, learning curve is flatter.
The best example, is that in 3.6 and lower, you had to do "loop = asyncio.get_event_loop()", then call "loop.run_until_complete()". In 3.7, you use asyncio.run(). It seems trivial, it's really not:
https://github.com/python/asyncio/blob/db2fe1d1e1d0f7352167a....
This has consequences on the whole design, which is now full of things like that (run_in_executor, create_task, run_forever, etc), changing completly the way to write correct code.
Another thing is that in 3.7, the asyncio uses async / await and not yield from. Because of this dog fooding, the whole lib improved.
3.7 has a LOT of patches just for asyncio: https://docs.python.org/3.7/whatsnew/changelog.html#python-3...
Speed (Future has been rewritten in C, and many other tweaks), fixes, _way_ better doc...
Obvious shortcomming were resolved, like asyncio.wait_for() that now waits until the cancellation is complete, or asyncio.all_tasks() returning only pending tasks. A lot of little things that add up.
The whole ergonomics are better, up to the command line experience with "python -X dev" which automatically add warnings and set asyncio in debug mode, and frankly you can't cleanly code async without.
The final nail in the coffin are the context vars (https://docs.python.org/3/library/contextvars.html). It's the only sane way to deal with a global registery that is dependant of the current running task.
It's almost always easier to upgrade to 3.7 than trying to use asyncio before it.