TLDR: for and with are non-trivial syntactic sugar that encapsulate several steps of calling related methods. This makes it impossible to manually add awaits between these steps – but properly usable async for/with need that. At the same time, this means it is vital to have async support for them.
Why we can't await nice things
Python's statements and expressions are backed by so-called protocols: When an object is used in some specific statement/expression, Python calls corresponding "special methods" on the object to allow customization. For example, x in [1, 2, 3] delegates to list.__contains__ to define what in actually means.
Most protocols are straightforward: There is one special method called for each statement/expression. If the only async feature we have is the primitive await, then we can still make all these "one special method" statements/expression "async" by sprinkling await at the right place.
In contrast, the for and with statements both correspond to multiple steps: for uses the iterator protocol to repeatedly fetch the __next__ item of an iterator, and with uses the context manager protocol to both enter and exit a context.
The important part is that both have more than one step that might need to be asynchronous. While we could manually sprinkle an await at one of these steps, we cannot hit all of them.
The easier case to look at is with: we can address at the __enter__ and __exit__ method separately.
We could naively define a syncronous context manager with asynchronous special methods. For entering this actually works by adding an await strategically:
with AsyncEnterContext() as acm: context = await acm print("I entered an async context and all I got was this lousy", context)
However, it already breaks down if we use a single with statement for multiple contexts: We would first enter all contexts at once, then await all of them at once.
with AsyncEnterContext() as acm1, AsyncEnterContext() as acm2: context1, context2 = await acm1, await acm2 # wrong! acm1 must be entered completely before loading acm2 print("I entered many async contexts and all I got was a rules lawyer telling me I did it wrong!")
Worse, there is just no single point where we could await exiting properly.
While it's true that for and with are syntactic sugar, they are non-trivial syntactic sugar: They make multiple actions nicer. As a result, one cannot naively await individual actions of them. Only a blanket async with and async for can cover every step.
Why we want to async nice things
Both for and with are abstractions: They fully encapsulate the idea of iteration/contextualisation.
Picking one of the two again, Python's for is the abstraction of internal iteration – for contrast, a while is the abstraction of external iteration. In short, that means the entire point of for is that the programmer does not have to know how iteration actually works.
Bottom line is the entire point of for – and with – is not to bother with implementation details. That includes having to know which steps we need to sprinkle with async. Only a blanket async with and async for can cover every step without us knowing which.
Why we need to async nice things
A valid question is why for and with get async variants, but others do not. There is a subtle point about for and with that is not obvious in daily usage: both represent concurrency – and concurrency is the domain of async.
Without going too much into detail, a handwavy explanation is the equivalence of handling routines (()), iterables (for) and context managers (with). As has been established in the answer cited in the question, coroutines are actually a kind of generators. Obviously, generators are also iterables and in fact we can express any iterable via a generator. The less obvious piece is that context managers are also equivalent to generators – most importantly, contextlib.contextmanager can translate generators to context managers.
To consistently handle all kinds of concurrency, we need async variants for routines (await), iterables (async for) and context managers (async with). Only a blanket async with and async for can cover every step consistently.
forandwithjust syntax sugars forwhileandtry ... except" — Nope, far from it, they're each their own thing.forandwithinvoke methods on the objects you put in, which are supposed to return certain values immediately. Withasync forandasync with, these methods can be async, allowing them to do some non-blocking work.withstatement "is semantically equivalent to"try...except...finally. And you can easily implement aforloop withwhileandnext. Maybe they are not syntax sugars, but they are not that different either.awaitfor async__enter__/__exit__/__iter__/__next__if they're implicitly called by the "sugar"with/forstatements?forandwithencapsulate protocols for specific patterns involving specific methods, which you can replicate "manually" withwhileandtry..except..finally. But the point is exactly to make those patterns reusable instead of writing a ton of boilerplate every time. And since that boilerplate differs for async versions, you need specificasyncversions of them.