TLDR: There is no workaround as Python expects a specific type for many special methods. However, some special methods have async variants.
An async def
function is fundamentally a different kind of function. Similar to how a generator function evaluates to a generator, a coroutine function evaluates to an awaitable.
You can define special methods to be async, but that affects their return type. In the case of __str__
, you get an Awatable[str]
instead of a bare str
. Since Python requires a str
here, this results in an error.
>>> class AStr:
... async def __str__(self):
... return 'Hello Word'
>>> str(AStr())
TypeError: __str__ returned non-string (type coroutine)
This affects all special methods that are directly interpreted by Python: These include __str__
, __repr__
, __bool__
, __len__
, __iter__
, __enter__
and some others. Usually, if a special method relates to some internally used functionality (e.g. str
for direct display) or statement (e.g. for
needing an iterator) it cannot be async
.
Some special methods are not directly interpreted by Python. Examples include arithmetic operators (__add__
, __sub__
, ...), comparisons (__lt__
, __eq__
, ...) and lookup (__get__
, __getattribute__
, ...). Their return type can be any object, including awaitables.
You can define such special methods via async def
. This affects their return type, but only requires client code to await
them. For example, you can define +
to be used as await (a + b)
.
>>> def AWAIT(awaitable):
... """Basic event loop to allow synchronous ``await``"""
... coro = awaitable.__await__()
... try:
... while True:
... coro.send(None)
... except StopIteration as e:
... return e.args[0] if e.args else None
...
>>> class APlus:
... def __init__(self, value):
... self.value = value
... async def __add__(self, other):
... return self.value + other
...
>>> async def add(start, *values):
... total = start
... for avalue in map(APlus, values):
... total = await (avalue + total)
... return total
...
>>> AWAIT(add(5, 10, 42, 23))
80
A few special methods exist for the await
machinery and are expected to return an awaitable. This includes __aenter__
, __aexit__
, and __anext__
. Notably, __await__
must return an iterator, not an awaitable.
You can (and in most cases should) define these methods as async def
. If you need asynchronous capabilities in the corresponding sync special method, use the corresponding async special method with async def
. For example, you can define an asynchronous context manager.
>>> class AContext:
... async def __aenter__(self):
... print('async enter')
... async def __aexit__(self, exc_type, exc_val, exc_tb):
... print('async exit')
...
>>> async def scoped(message):
... async with AContext():
... print(message)
...
>>> AWAIT(scoped("Hello World"))
async enter
Hello World
async exit