Asyncio timeout on event wait

I had code like this:

    try:
        async with asyncio.timeout(delay):
            await event.wait()
    except TimeoutError:
        return "No event"
    return "Event!"

Unfortunately, it is not either timeout or event as I originally assumed. When the event and the timeout occur almost in the same time, we could have both the timeout and the event. The code below proved it. But as I said, it is timing sensitive.

    try:
        async with asyncio.timeout(delay):
            await event.wait()
    except TimeoutError:
        assert not event.is_set()    # <-- not always true!
        return "No event"
   ...

An await cannot both succeed and fail. It looks to me like other tasks can be run during timeout handling in the timeout context manager. I don’t know, I just cannot explain the event state change otherwise.

Anyway, in my case it introduced an ugly bug. This is how I corrected it:

    try:
        async with asyncio.timeout(delay):
            await event.wait()
    except TimeoutError:
        pass
    return "Event!" if event.is_set() else "No event"

I decided to post this because I would like to know if it can be considered a “gotcha” that should be somehow addressed, e.g. by enhancing the doumentation?

While there is indeed nothing preventing the event to be set after you’ve awaited it that shouldn’t be possible without some await between the wait and the check.

Is there an await in the real code?

Because otherwise it should only really be possible if the event is set by another thread which is not supported as Event like most asyncio structures are not thread safe.

Or maybe (I would have to check the code) it could be that we hit the TimeoutError even when the event is set if it had already expired when we first called wait

Edit:

Just checked and if the event is set when `wait` is called it doesn’t yield to the event loop and so as your examples above are written you really should’ve be able to get a `TimeoutError` unless you’re calling `set()` on the event from another thread which is a bug (use call_soon_threadsafe instead)

1 Like
  • I corrected a typo, there is async with, not just with as I wrote by mistake.
  • there is no other await
  • there are no other threads

Please find attached a reproducer. It stops with an assertion error on my PC and Python 3.11, 3.12, 3.13 and 3.14.

import asyncio

ev = asyncio.Event()

async def ev_wait(delay):
    try:
        async with asyncio.timeout(delay):
            await ev.wait()
    except TimeoutError:
        assert not ev.is_set(), "Bang!"

async def ev_send(delay):
    await asyncio.sleep(delay)
    ev.set()

async def main():
    for delta in range(-100, 100):
        ev.clear()
        t1 = asyncio.create_task(ev_wait(0.01))
        t2 = asyncio.create_task(ev_send(0.01 + delta/10000))
        await t1
        await t2

if __name__ == "__main__":
    asyncio.run(main())