The circular import problem in Python.
Some module foo
imports module bar
, but bar
also imports foo
.
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.
Example
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 Thing
:
# connection.py
from thing import Thing
class ApiConnection:
def get_thing(self) -> Thing:
return Thing(connection=self)
This 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'
In 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.
The typing.TYPE_CHECKING
constant makes this easily possible.
In our example we change thing.py
to:
# 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 Thing.__init__
.
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