'how to solve the recursion problem when specifying type hints for classes from different files

how to solve the recursion problem when specifying type hints for classes from different files

models1.py

from models2 import Second
@dataclass
class First:
    attribute: Second

models2.py

from models1 import First
@dataclass
class Second:
    attribute: First

In real code, I wanted to split SentInvoice and User models into different files.

class User(models.Model):
    user_id = fields.BigIntField(index=True, unique=True)
    username = fields.CharField(32, unique=True, index=True, null=True)
    first_name = fields.CharField(255, null=True)
    last_name = fields.CharField(255, null=True)
    language = fields.CharField(32, default="ru")
    balance: Balance = fields.OneToOneField("models.Balance", on_delete=fields.CASCADE)
    sent_invoice: fields.OneToOneNullableRelation["SentInvoice"] # here
    registered_user: RegisteredUser

    @classmethod
    async def create(cls: Type[MODEL], **kwargs: Any):
        return await super().create(**kwargs, balance=await Balance.create())


class SentInvoice(models.Model):
    amount = fields.DecimalField(17, 7)
    shop_id = fields.CharField(50)
    order_id = fields.CharField(10, null=True)
    email = fields.CharField(20, null=True)
    currency = fields.CharField(5, default="RUB", description="USD, RUB, EUR, GBP")
    user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField("models.User", on_delete=fields.CASCADE)  # here
    created_invoice: fields.OneToOneNullableRelation[CreatedInvoice]

    async def send(self) -> CreatedInvoice:
        cryptocloud = config.payment.cryptocloud
        async with aiohttp.ClientSession(headers={"Authorization": f"Token {cryptocloud.api_key}"}) as session:
            async with session.post(cryptocloud.create_url, data=dict(self)) as res:
                created_invoice = await CreatedInvoice.create(**await res.json(), sent_invoice=self)
                return created_invoice


Solution 1:[1]

You need to use two techniques that are specific to type hinting in python, 1) forward references, and 2) importing types within a TYPE_CHECKING guard (check e.g. this post for a longer explanation of its implications). The first one allows you to reference a type that is not know to the interpreter at runtime, and the latter resolves types in a "type checking context".

Long story short:

models1.py

from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models2 import Second

@dataclass
class First:
    attribute: "Second"

models2.py

from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models1 import First

@dataclass
class Second:
    attribute: "First"

Executing the files with python3.8 or higher should work without any issues[1], and can work in python3.7 as well with a __futures__ import. Running mypy on the files should work without any issues, too:

$ mypy models1.py models2.py
Success: no issues found in 2 source files 

[1] As comments have pointed out, creating actual instances of your First/Second classes that would also pass a type check is impossible, but I assume that this is a toy example and your real code has, for example, one of the attribues as Optional.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1