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