why you must use async with
It's not like you must use async with, it's just a fail-safe device for ensuring that the resources get cleaned up. Taking a classic example from the documentation:
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
You can re-write it as:
async def fetch(session, url):
response = await session.get(url)
return await response.text()
This version appears to work the same, but it doesn't close the response object, so some OS resources (e.g. the underlying connection) may continue to be held indefinitely. A more correct version would look like this:
async def fetch(session, url):
response = await session.get(url)
content = await response.text()
response.close()
return content
This version would still fail to close the response if an exception gets raised while reading the text. It could be fixed by using finally - which is exactly what with and async with do under the hood. With an async with block the code is more robust because the language makes sure that the cleanup code is invoked whenever execution leaves the block.