There is an approach that combines a __future__ import to disregard type annotations at runtime, with a if TYPE_CHECKING clause that "imports" the code from your IDE's point of view only, so that code completion is available.
Example:
my_number.py
class MyNumber:
def __init__(self):
self.x = 5
from my_method import my_method
my_method.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from my_number import MyNumber
def my_method(self: MyNumber):
print(self.x)
With the from __future__ import annotations, we postpone the evaluation of type hints - in other words, we can type hint my_method even if we don't actually import MyNumber. This behavior was planned to be the default in Python 3.10, but it got postponed, so we need this import for now.
Now depending on your editor/IDE, you will still get a warning complaining that MyNumber isn't defined, and its methods/attributes may not show up on the autocomplete. Here's where the TYPE_CHECKING comes into play: this is simply a constant False value, which means that our clause is:
if False:
from my_number import MyNumber
In other words, we're "tricking" the IDE into thinking we're importing MyNumber, but in reality that line never executes. Thus we avoid the circular import altogether.
This might feel a little hacky, but it works :-) the whole point of the TYPE_CHECKING constant is to allow type checkers to do their job, while not actually importing code at runtime, and to do so in a clear way (Zen of Python: There should be one-- and preferably only one --obvious way to do it).
This approach has worked for me consistently in PyCharm, not sure about other IDEs/editors.