Home > Net >  How to correctly type an asyncio class instance variables
How to correctly type an asyncio class instance variables

Time:10-13

Consider the following example class containing attributes that require running a coroutine for initialization:

class Example:
  def __init__(self) -> None:
    self._connection: Optional[Connection] = None

  async def connect() -> None:
    self._connection = await connect_somewhere(...)

  async def send(data: bytes) -> None:
    self._connection.send(data)

If I run mypy (perhaps with strict-optional enabled) on this example, it will complain that _connection can be None in send method and the code is not type-safe. I can't initialize the _connection variable in __init__, as it needs to be run asynchronously in a coroutine. It's probably a bad idea to declare the variable outside __init__ too. Is there any way to solve this? Or do you recommend another (OOP) design that would solve the issue?

Currently, I either ignore the mypy complaints, prepend assert self._connection before each usage or append # type: ignore after the usage.

CodePudding user response:

It is generally not good design to have classes in an unusable state unless some method is called on them. An alternative is dependency injection and an alternative constructor:

from typing import TypeVar, Type

# not strictly needed – one can also use just 'Example'
# if inheritance is not needed
T = TypeVar('T')

class Example:
    # class always receives a fully functioning connection
    def __init__(self, connection: Connection) -> None:
        self._connection = connection

    # class can construct itself asynchronously without a connection
    @classmethod
    async def connect(cls: Type[T]) -> T:
        return cls(await connect_somewhere(...))

    async def send(data: bytes) -> None:
        self._connection.send(data)

This frees __init__ from relying on some other initialiser to be called later on; as a bonus, it is possible to provide a different connection, e.g. for testing.

The alternative constructor, here connect, still allows to create the object in a self-contained way (without the callee knowing how to connect) but with full async support.

async def example():
    # create instance asynchronously
    sender = await Example.connect()
    await sender.send(b"Hello ")
    await sender.send(b"World!")

CodePudding user response:

It's probably a bad idea to declare the variable outside __init__ too

This is close. You have to annotate it outside of __init__.

class Example:
    _connection: Connection

    async def connect(self) -> None:
        self._connection = await connect_somewhere(…)
  • Related