Python objects are synchronous by default. When working with asyncio if we create an object the __init__ is a regular function and we cannot do any async work in here.
import asyncio
class Hello:
def __init__(self):
print("init")
# We cannot await anything in here
async def method(self):
print("method")
# We can await in here
async def main():
h = Hello()
await h.method()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Running this code will print:
init
method
Awaitable objects
We can make our object directly awaitable by giving it an __await__ method. This method must return an iterator.
When defining an async function the __await__ method is created for us, so we can create an async closure and use the __await__ method from that.
import asyncio
class Hello:
def __init__(self):
print("init")
# We cannot await anything in here
def __await__(self):
async def closure():
print("await")
# We can await in here
return self
return closure().__await__()
async def method(self):
print("method")
# We can await in here
async def main():
h = await Hello()
await h.method()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Here we’ve added the __await__ method and updated our object creation to be h = await Hello().
Running this code will print:
init
await
method
Async context managers
We can also turn our object into an async context manager with __aenter__ and __aexit__ coroutines.
import asyncio
class Hello:
def __init__(self):
print("init")
def __await__(self):
async def closure():
print("await")
return self
return closure().__await__()
async def __aenter__(self):
print("enter")
return self
async def __aexit__(self, *args):
print("exit")
async def method(self):
print("method")
async def main():
async with Hello() as h:
print("context")
await h.method()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Here we’ve added the context manager methods and updated our object creation to be an async with statement.
Running this code will print:
init
enter
context
method
exit
Notice that while our enter and exit coroutines were called as expected our object is never awaited.
We can fix this by awaiting it ourselves within the __aenter__ method.
import asyncio
class Hello:
def __init__(self):
print("init")
def __await__(self):
async def closure():
print("await")
return self
return closure().__await__()
async def __aenter__(self):
await self
print("enter")
return self
async def __aexit__(self, *args):
print("exit")
async def method(self):
print("method")
async def main():
async with Hello() as h:
print("context")
await h.method()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Running this code will print:
init
await
enter
context
method
exit
Wrap up
With our new context manager class the only place we cannot use async code is within the __init__ method. Which is completely reasonable as we should only ever be setting up our object’s attributes in there anyway.
Hopefully this article has given a quick overview on creating awaitable objects and async context managers and also shown the order of operations when using one.