What must an object implement for `await obj` to be legal in Python?
answer
- Await accepts more than coroutines
- It is a structural protocol
- One dunder, returning an iterator
- StopIteration carries the result value
- Futures and tasks implement it
basics
~10 sAwait accepts any awaitable: a coroutine object, or any object whose await method returns an iterator. The value the iterator finally carries in StopIteration becomes the result of the await. Anything else raises TypeError.
solid answer
~40 s`await` requires an *awaitable*. Three kinds qualify: a coroutine object from an `async def` call; any object defining `__await__` as an ordinary method returning an iterator; and legacy generator-based coroutines marked with `types.coroutine`. `asyncio.Future` and its subclass `asyncio.Task` are awaitable precisely because they implement `__await__`. The protocol is deliberately generator-shaped: awaiting delegates to that iterator, each value it yields travels out to whatever is driving the coroutine - for asyncio, the event loop - and when the iterator stops, the value attached to `StopIteration` becomes the result of the `await` expression. `collections.abc.Awaitable` is the abstract base for the concept and `inspect.isawaitable` the runtime check. Awaiting a non-awaitable raises `TypeError`, whose message on 3.14 reads `'int' object can't be awaited`.
code
python · 14 linesimport asyncio
class Ready:
def __init__(self, value):
self.value = value
def __await__(self): # a plain def returning an iterator
yield # suspend once, hand the loop a turn
return self.value # becomes the await result
async def main():
print(await Ready(42))
asyncio.run(main())go deeper
Know the word: an awaitable is anything await accepts, which in everyday code means a coroutine object or a task. Awaiting a plain value or a function object raises TypeError.
Explain the protocol itself: await is a normal method returning an iterator, the iterator's yields pass out to whatever drives the coroutine, and its StopIteration value becomes the await result.
Show where it earns its keep: futures and tasks are awaitable through this protocol, and adapters wrapping callback APIs implement it. Be able to say why such awaitables are rarely portable across event-loop frameworks.
Judge when a custom awaitable is worth it at all. A coroutine function is simpler and reads better; the protocol pays off for objects that must serve two call styles or bridge a foreign API, and it ties the code to one loop implementation.
### Awaitable is a protocol, not a type `await` is not restricted to coroutine objects. Its operand must be an **awaitable**, which the language defines structurally. Three categories satisfy it: * **Coroutine objects** returned by calling an `async def` function. * **Objects implementing `__await__`** - a plain (non-async) method returning an iterator. * **Generator-based coroutines**, ordinary generator functions decorated with `types.coroutine`. This is the legacy path from before native syntax; the decorator asyncio once provided for the same purpose was removed in 3.11, but `types.coroutine` remains and is still how some low-level library code hands raw yields to the loop. `collections.abc.Awaitable` is the abstract base class for the concept - a class with `__await__` is a virtual subclass of it - and `inspect.isawaitable(obj)` is the runtime predicate. Note that a coroutine object satisfies it while a coroutine *function* does not: the function is not awaitable, its return value is. ### Why an iterator The design deliberately reuses the generator machinery. Driving a coroutine means repeatedly calling `send` on it and receiving whatever it yields. When execution hits `await x`, control delegates into `x.__await__()`, and every value that iterator yields passes straight through to the driver. When the iterator raises `StopIteration`, its `value` becomes the value of the `await` expression: ```python class Constant: def __init__(self, value): self.value = value def __await__(self): return iter([]) # never suspends # a `return self.value` inside a generator would set StopIteration.value ``` The practically useful version is a generator function, where `return` sets the result: ```python class Ready: def __init__(self, value): self.value = value def __await__(self): yield # suspend once; give the loop a turn return self.value # becomes the result of `await Ready(x)` ``` Two mistakes are common here. `__await__` must **not** be `async def` - that would return a coroutine object rather than an iterator, and awaiting the instance fails. And whatever is yielded is interpreted by whichever framework is driving; `asyncio` treats a bare `yield` as "relinquish one loop iteration" and a yielded `Future` as "wake me when this completes". Yielding some other object to an asyncio loop is an error, which is why awaitables written for one event-loop framework are usually not portable to another. ### Where the protocol shows up in practice You rarely write `__await__` in application code, and that is exactly why this is a differentiator rather than a gate. Where it matters: * **Futures and tasks.** `asyncio.Future` implements `__await__`, and `asyncio.Task` inherits it. That is the whole mechanism by which `await some_task` works: it is not special-cased syntax, just the protocol. * **Objects that are usable both ways.** A method can return an object that is both awaitable and, say, a context manager or an iterable, so the same call reads naturally in more than one style. Library code does this to offer `await conn.execute(...)` and `async with conn.execute(...)` from one return value. * **Adapters.** Wrapping a callback-based API in an object whose `__await__` yields a future is the standard bridge from callback style into async/await. ### Neighbouring dunders worth distinguishing `__await__` is not the only async protocol, and interviewers sometimes probe the family. `__aenter__` and `__aexit__` make an *asynchronous* context manager, used with `async with`; both are `async def`, so they return awaitables. `__aiter__` and `__anext__` make an asynchronous iterator for `async for`; `__anext__` is awaited each round and signals exhaustion with `StopAsyncIteration`. All three protocols ultimately funnel into the same rule: the thing that gets awaited must be an awaitable, so a coroutine object or an `__await__` implementation. ### Failure modes Awaiting a non-awaitable produces a clear message - on 3.14, `TypeError: 'int' object can't be awaited` - and the most frequent real cause is awaiting the *result* of an already-awaited call, or awaiting a synchronous function's return value. The other classic is awaiting a coroutine *function* rather than calling it first: `await fetch` instead of `await fetch()`, which raises `TypeError: 'function' object can't be awaited`. ### When to reach for it Most application code never needs `__await__`. A coroutine function is shorter, reads better, and cannot be mis-driven, so the honest answer to "should I write a custom awaitable?" is usually no. The protocol earns its place in three narrow situations: an object that must be usable both as `await x` and in another style such as `async with x`; a bridge that turns a callback-registration API into something awaitable by yielding a future the callback later resolves; and low-level library code that needs to speak directly to the loop's driver. Each of those buys something a coroutine function cannot express. Everything else is complexity you will pay for at the next loop-framework change.
- Why must `__await__` be a normal `def` rather than an `async def`?Because the protocol requires an iterator, and `async def` returns a coroutine object instead. The await machinery calls `__await__()` and then drives the returned object with the iterator protocol; handing it a coroutine breaks that contract and the await fails. Write it as a generator function - `yield` to suspend, `return` to deliver the result - or return an existing iterator such as another awaitable's `__await__()`.
- How does `await some_task` work if Task is not a coroutine?`asyncio.Future` implements `__await__`, and `asyncio.Task` subclasses Future, so both are awaitable through the ordinary protocol rather than by special syntax. Awaiting one yields the future itself out to the loop, which registers the awaiting coroutine as a callback and resumes it when the result is set. That is also why the same object can be awaited from several places.
- Is a custom awaitable portable between event-loop frameworks?Usually not. The protocol standardises the shape - an iterator whose yields travel out to the driver - but not the meaning of the yielded values. asyncio interprets a bare yield as one loop turn and a yielded Future as a wait; another framework defines its own vocabulary. Awaitables that only delegate to other awaitables are portable; those that yield framework-specific objects are not.
saying these in an interview costs you the question
- Says only coroutine objects can be awaited
- Writes __await__ as an async def method
- Thinks a coroutine function itself is awaitable
- Believes __await__ should return the final value directly
- Confuses __await__ with __aiter__ or __aenter__