To my surprise, no one has mentioned cyclic imports caused by type hints yet.
If you have cyclic imports only as a result of type hinting, they can be avoided in a clean manner.
Consider main.py
which makes use of exceptions from another file:
from src.exceptions import SpecificException
class Foo:
def __init__(self, attrib: int):
self.attrib = attrib
raise SpecificException(Foo(5))
And the dedicated exception class exceptions.py
:
from src.main import Foo
class SpecificException(Exception):
def __init__(self, cause: Foo):
self.cause = cause
def __str__(self):
return f'Expected 3 but got {self.cause.attrib}.'
This will raise an ImportError
since main.py
imports exception.py
and vice versa through Foo
and SpecificException
.
Because Foo
is only required in exceptions.py
during type checking, we can safely make its import conditional using the TYPE_CHECKING
constant from the typing module. The constant is only True
during type checking, which allows us to conditionally import Foo
and thereby avoid the circular import error.
Something to note is that by doing so, Foo
is not declared in exceptions.py at runtime, which leads to a NameError
. To avoid that, we add from __future__ import annotations
which transforms all type annotations in the module to strings.
Hence, we get the following code for Python 3.7+:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING: # Only imports the below statements during type checking
from src.main import Foo
class SpecificException(Exception):
def __init__(self, cause: Foo): # Foo becomes 'Foo' because of the future import
self.cause = cause
def __str__(self):
return f'Expected 3 but got {self.cause.attrib}.'
In Python 3.6, the future import does not exist, so Foo
has to be a string:
from typing import TYPE_CHECKING
if TYPE_CHECKING: # Only imports the below statements during type checking
from src.main import Foo
class SpecificException(Exception):
def __init__(self, cause: 'Foo'): # Foo has to be a string
self.cause = cause
def __str__(self):
return f'Expected 3 but got {self.cause.attrib}.'
In Python 3.5 and below, the type hinting functionality did not exist yet.
In future versions of Python, the annotations feature may become mandatory, after which the future import will no longer be necessary. Which version this will occur in is yet to be determined.
This answer is based on Yet another solution to dig you out of a circular import hole in Python by Stefaan Lippens.
AttributeError
s - it enables looking up the partially initialized module insys.modules
, but doesn't resolve time paradoxes. – Brantbrantford