[...] we already have line 'a' where we are assigning a value to the result variable.
Not really. To be more precise, we are doing multiple things in line a in that order:
- First, we are calling
asyncio.shield(long_task), which returns a new Future (let's name it f).
- Next, we are calling
asyncio.wait_for with f, which returns a new coroutine object (let's name it c).
- Then, we are
awaiting c, which returns whatever the underlying function wait_for returns. (Which in this case is what shield would return, which in turn is just what long_task returns.)
- Lastly, we assign that return value from step 3 to the name
result (which happens to be local to the main function).
As is the case with any combination of Python expressions/assignments, if any preceding operation raises an exception, the interpreter does not proceed with the following operations. The try-except-construct allows us to define what happens after that. But it does not change the fact that everything starting from the line that caused the exception up until our except-block is completely disregarded.
[...] TimeoutError exception is raised before the future in line 'a' is completed
Yes, but you need to be specific about which future you are talking about. In this case wait_for does in fact complete (with the exception). And before raising it cancels future f (returned by shield), but long_task (having been shielded from cancellation) is not done at this point.
In this concrete example, during step 3 the TimeoutError exception is raised. This means that whatever comes after that, i.e. the variable assignment in step 4 is not executed.
[...] since no value was assigned to result in the try clause, the except clause will raise UnboundLocalError.
Exactly. At runtime, the result name is never created in the try-block.
And this is what I think @user2357112 meant by everything relevant being the same with synchronous code.
It is just that in this case you may get confused by the nested futures because long_task is still running despite the exception having been raised. Nothing cancelled it, so in our except-block we can treat it the same as before. Thus we can for example await it directly (as you did) and assign its output to result. This is something we could not do in synchronous code of course.
PS
Regarding your follow-up question, yes, long_task is still the same task. Its execution began earlier, when you called create_task and it remained unaffected by the exception. wait_for actually cancels the task it was passed, if the timeout is reached, but this did not affect long_task because you shielded it from cancellation. Of course, if you don't catch the exception, but allow it to bubble up, that will interrupt the entire program (including the event loop).
You can actually test fairly easily, that you are dealing with the same task:
from asyncio import create_task, run, shield, sleep, wait_for
from asyncio.exceptions import TimeoutError
async def wait(n: int) -> str:
print("Starting to wait")
for i in range(n):
await sleep(1)
print(f"Waited {i + 1} sec")
print("Done waiting")
return "foo"
async def main() -> None:
long_task = create_task(wait(5))
try:
result = await wait_for(shield(long_task), timeout=1.5)
except TimeoutError:
print("Taking longer than usual. Please wait.")
result = await long_task
print(result)
if __name__ == "__main__":
run(main())
Output:
Starting to wait
Waited 1 sec
Taking longer than usual. Please wait.
Waited 2 sec
Waited 3 sec
Waited 4 sec
Waited 5 sec
Done waiting
foo
tryclause wasn't actually unsuccessful per se. It just didn't finish within stipulated time frame. So my question is about the fate/result oftryclause after it raises an exception which we handle inexceptclause. In above case, what happens toresultvariable intryclause after an exception is raised.