The circular import problem in Python.
foo imports module
bar also imports
On itself, it's not necessarily a problem. Python allows it.
Depending on how both modules interact,
you might not even notice there is cycle in the dependency chain.
However, when you have a problem, some serious hair-pulling might ensue.
There are enough places around the web that elaborate on this issue, play the blame game ("bad design!") and offer possible solutions. These solutions have varying degrees of ugliness or architectural itchiness: rethink your coupling/dependencies, introduce abstract interfaces, merge modules, split modules, use local imports, defer imports, etc. Sometimes a cleaner design or better decoupling might indeed get you out of the circular import hole. But sometimes there is an inherent circular dependency and the only way out are ugly hacks. But we digress.
Circular imports and type hinting
Since I started using type hinting more, I noticed that it is easier to get into circular import troubles. It's not so unexpected since class type hints typically require you to import more. Also, they have to be imported at top level so you can not leverage tricks with local or deferred imports.
When you have a circular import issue only because of type hinting, there is lesser known solution I want to show here.
Let's take this simple artificial example with two modules that
need each other:
connection.py defines an interface to something
like a REST API, and it can create some kind of entity in this API, called
# connection.py from thing import Thing class ApiConnection: def get_thing(self) -> Thing: return Thing(connection=self)
Thing (defined in
thing.py) keeps a reference to the connection
so that operations on a
Thing can be send to the API.
# thing.py from connection import ApiConnection class Thing: def __init__(self, connection: ApiConnection): self._conn = connection
No surprise that the circular dependency here will cause failure, resulting in a classic circular import stack trace like:
File "main.py" from connection import ApiConnection File "connection.py" from thing import Thing File "thing.py" from connection import ApiConnection ImportError: cannot import name 'ApiConnection' from 'connection'
thing.py, the import of the
connection module is only there for the
ApiConnection type hint.
That means that if you don't care too much about that type hint,
you could drop it an break the cycle of doom.
Conditional import for type hints
A lesser know solution for this case is to use a conditional import
that is only active in "type hinting mode", but doesn't interfere at run time.
constant makes this easily possible.
In our example we change
# thing.py from typing import TYPE_CHECKING if TYPE_CHECKING: from connection import ApiConnection class Thing: def __init__(self, connection: 'ApiConnection'): self._conn = connection
The code will now execute properly as there is no circular import issue anymore.
Type hinting tools on the other hand should still be
able to resolve the
ApiConnection type hint in
My current IDE (PyCharm) for example picks it up just fine for code intelligence features.
Unfortunately the type hint has to be specified as a "forward reference" string
which is bit uglier than a normal type hint.
Since Python 3.7 however, thanks to
PEP563 Postponed Evaluation of Annotations it is possible to specify the type hint without the quotes, but that requires an additional import first:
from __future__ import annotations