I guess I was focusing more on the CPU task problem mentioned at the top. But you're talking about blocking IO - more specifically, blocking IO that you want to convert to async (not just run in a worker thread using the thread-to-async API I mentioned).
What you've written is true, but "infects" a smaller proportion of the code than you'd expect, in my experience. In fact, the better organised the program is, the less needs to change when converting to async. For example, reading from a connection your top-level async function would often be something like this:
async def read_and_handle_messages(connection):
while True:
next_message_bytes = await connection.get_next_message()
next_message_parsed = my_parser.parse(next_message_bytes)
my_message_handler(next_message_parsed)
All the application-specific code is in the parser and the message handler but neither of those are async. (If you need to send a response, your message handler could post a message to an async queue that is read by a separate writer task.) In your hypothetical scenario, you'd maybe write two a wrapper functions, one for blocking and one for async, which both read the message, parse it and handle it. But that only needs to be two versions of a three line function while all the subtantial code is completely shared.
I find that there's very little actual async code in async programs that I write, even with no blocking IO historical baggage. Admittedly, that's partly because I organise programs to reach that goal, but it actually works out for the best because all the async-task lifecycle management ends up in one place, which makes following overall program flow particularly easy.