python/cpython · error · ValueError

Set of Tasks/Futures is empty.

Error message

Set of Tasks/Futures is empty.

What it means

ValueError raised by asyncio.wait() when the collection of awaitables is empty. With nothing to wait on there is no meaningful event to suspend for, and returning immediately could mask logic errors, so the API refuses the call.

Source

Thrown at Lib/asyncio/tasks.py:422

async def wait(fs, *, timeout=None, return_when=ALL_COMPLETED):
    """Wait for the Futures or Tasks given by fs to complete.

    The fs iterable must not be empty.

    Returns two sets of Future: (done, pending).

    Usage:

        done, pending = await asyncio.wait(fs)

    Note: This does not raise TimeoutError! Futures that aren't done
    when the timeout occurs are returned in the second set.
    """
    if futures.isfuture(fs) or coroutines.iscoroutine(fs):
        raise TypeError(f"expect a list of futures, not {type(fs).__name__}")
    if not fs:
        raise ValueError('Set of Tasks/Futures is empty.')
    if return_when not in (FIRST_COMPLETED, FIRST_EXCEPTION, ALL_COMPLETED):
        raise ValueError(f'Invalid return_when value: {return_when}')

    fs = set(fs)

    if any(coroutines.iscoroutine(f) for f in fs):
        raise TypeError("Passing coroutines is forbidden, use tasks explicitly.")

    loop = events.get_running_loop()
    return await _wait(fs, timeout, return_when, loop)


def _release_waiter(waiter, *args):
    if not waiter.done():
        waiter.set_result(None)


async def wait_for(fut, timeout):

View on GitHub (pinned to bc6749cc3b)

Solutions

  1. Guard the empty case before calling: if not tasks: return set(), set() (or skip the wait entirely)
  2. When draining dynamically produced work, loop while the workset is non-empty instead of calling wait unconditionally
  3. If you only need 'wait until all currently known tasks finish', prefer asyncio.gather(*tasks) which accepts zero args only via an empty call — validate the same way

Example fix

// before
done, pending = await asyncio.wait(running)  # running can be []

// after
if running:
    done, pending = await asyncio.wait(running)
else:
    done, pending = set(), set()
Defensive patterns

Strategy: validation

Validate before calling

async def wait_all(tasks):
    tasks = list(tasks)
    if not tasks:
        return set(), set()
    return await asyncio.wait(tasks)

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: await asyncio.wait([]); asyncio.wait(tasks) where tasks was built from a filter that matched nothing (e.g. all work already completed, an empty queue drain, zero subscribers).

Common situations: Fan-out loops over dynamic worksets that occasionally empty out; batch pipelines after the final batch; code that assumed wait([]) blocks or returns (done=set(), pending=set()) without error.

Related errors


AI-assisted analysis of python/cpython@bc6749cc3b (2026-08-14). Data as JSON: /api/errors/f2216f34a7a5ce90. Report an issue: GitHub.