The asyncio API
Every asyncio call a round expects, one at a time: what it returns, what it waits on, what a timeout does, what it raises, the mistake people make with it, and a test that pins it down.
Should I use asyncio.gather or asyncio.TaskGroup?
In new code on Python 3.11 or later, TaskGroup. When one task fails, a TaskGroup cancels the others, waits for them, and raises every failure together in an ExceptionGroup. gather hands the first exception to the caller and leaves the other tasks running, so a failure can leave work going on in the background with nobody waiting for it. gather is still fine when you want every result in order and pass return_exceptions=True to collect failures as values.
Do I need a lock in asyncio if there is only one thread?
Only when an await sits between a check and the action that depends on it. A task is switched out only at an await, so code with no await between reading a value and writing it back runs without any other task interleaving. Put an await in between, and another task can run there and change the value you checked. Then you need an asyncio.Lock held across the check, the await and the write.
Why is swallowing CancelledError a bug?
Because asyncio.timeout, wait_for and TaskGroup all work by cancelling a task and waiting for the CancelledError to come back out. If your code catches it and returns normally, the timeout never fires for the caller: the block ends as if the work had finished. Clean up in finally, or catch CancelledError, clean up and raise it again. CancelledError derives from BaseException since Python 3.8, so except Exception does not catch it.
What happens if I call a blocking function inside a coroutine?
The whole event loop stops until it returns. Every other task waits, including any task that would have produced what the blocking call is waiting for. Run blocking work on a thread with asyncio.to_thread or loop.run_in_executor, and the loop keeps running other tasks meanwhile.
How do I test asyncio code without a plugin?
Write a plain test function that calls asyncio.run on a coroutine, and assert on what it returns. Force any order you depend on with asyncio.Event, never with a sleep of a real duration, and wrap every await that could wait forever in asyncio.timeout so a broken test fails instead of hanging.