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.
AttributeErrors - it enables looking up the partially initialized module insys.modules, but doesn't resolve time paradoxes.