* One coroutine can only interrupt another one at a point clearly marked with await (or async for or async with). That makes it easier to avoid data races without explicit synchronisation like locks.
* It's much easier to spawn async tasks and avoid them getting lost than with threads, assuming you use asyncio task groups (either by using a future version of Python, or using the anyio library now, or using Trio instead of asyncio).
* Async operations all have first class support for cancellation, and this interacts really cleanly with task groups. That helps with things like time outs, clean shutdown of your program, or cleaning up all resources related to a connection when that connection is closed.
* There's a bit more boilerplate in spawning threads and exchanging messages with them and joining them than the is spawning async tasks, especially when using task groups. (Admittedly, this is a solvable problem, and there are probably good libraries out there to help with this.)