When I have the following minimum reproducing code:
start.py
from __future__ import annotations
import a
a.py
from __future__ import annotations
from typing import Text
import b
Foo = Text
b.py
from __future__ import annotations
import a
FooType = a.Foo
I get the following error:
soot@soot:~/code/soot/experimental/amol/typeddict-circular-import$ python3 start.py
Traceback (most recent call last):
File "start.py", line 3, in <module>
import a
File "/home/soot/code/soot/experimental/amol/typeddict-circular-import/a.py", line 5, in <module>
import b
File "/home/soot/code/soot/experimental/amol/typeddict-circular-import/b.py", line 5, in <module>
FooType = a.Foo
AttributeError: partially initialized module 'a' has no attribute 'Foo' (most likely due to a circular import)
I included __future__.annotations
because most qa of this sort is resolved by simply including the future import at the top of the file. However, the annotations import does not improve the situation here because simply converting the types to text (as the annotations import does) doesn't actually resolve the import order dependency.
More broadly, this seems like an issue whenever you want to create composite types from multiple (potentially circular) sources, e.g.
CompositeType = Union[a.Foo, b.Bar, c.Baz]
What are the available options to resolve this issue? Is there any other way to 'lift' the type annotations so they are all evaluated after everything is imported?
In most cases using typing.TYPE_CHECKING
should be enough to resolve circular import issues related to use in annotations.
Note annotations future-import (details), alternatively you can enclose all names not available at runtime (imported under if TYPE_CHECKING
) in quotes.
# a.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from b import B
class A: pass
def foo(b: B) -> None: pass
# b.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from a import A
class B: pass
def bar(a: A) -> None: pass
# __main__.py
from a import A
from b import B
However, for exactly your MRE it won't work. If the circular dependency is introduced not only by type annotations (e.g. your type aliases), the resolving may become really tricky.
If you don't need Foo
available at runtime in your example, it can be declared in if TYPE_CHECKING:
block too, mypy will interpret that properly. If it is for runtime too, then everything depends on exact code structure (in your MRE dropping import b
is enough). Union type can be declared in separate file that imports a
, b
and c
and creates Union. If you need this union in a
, b
or c
, then things are a bit more complicated, probably some functionality needs to be extracted into separate file d
that creates union and uses it (also the code will be a bit cleaner this way, because every file will contain only common functionality).