There are a bunch of these python
circular import questions on the web. I chose to contribute to this thread because the query has a comment by Ray Hettinger that legitimizes the use case of a circular import, but recommends a solution that I believe is not especially good practice - moving the import to a method.
Apart from Hettinger's authority, three disclaimers to common objections are necessary:
- I've never programmed in Java. I'm not trying to do Java style.
- Refactoring is not always useful or effective. Logical API sometimes dictates a structure that makes recursive import references unavoidable. Remember, code exists for users, not programmers.
- Combining modules that are quite large can cause readability and maintainability issues that may be much worse than one or two recursive imports.
In addition, I believe maintainability and readability dictates that imports be grouped at the top of the file, occur only once for each needed name, and that the from module import name
style is preferable (except perhaps for very short module names with many functions, e.g. gtk
), as it avoids repetitive verbal clutter and makes dependencies explicit.
With that out of the way, I will posit a simplified version of my own use case that brought me here, and provide my solution.
I have two modules, each defining many classes. surface
defines geometric surfaces like planes, spheres, hyperboloids, etc. path
defines planar geometric figures like lines, circles hyperbolae, etc. Logically, these are distinct categories and refactoring is not an option from the perspective of API requirements. Nevertheless, these two categories are intimate.
A useful operation is intersecting two surfaces, for example, the intersection of two planes is a line, or the intersection of a plane and a sphere is a circle.
If for example, in surface.py
you do the straight forward import needed to implement the return value for an intersection operation:
from path import Line
you get:
Traceback (most recent call last):
File "surface.py", line 62, in <module>
from path import Line
File ".../path.py", line 25, in <module>
from surface import Plane
File ".../surface.py", line 62, in <module>
from path import Line
ImportError: cannot import name Line
Geometrically, planes are used to define the paths, after all, they may be arbitrarily oriented in three (or more) dimensions. The traceback tells you both what is happening and the solution.
Simply replace the import statement in surface.py
with:
try: from path import Line
except ImportError: pass # skip circular import second pass
The sequence of operations in the trace back is still happening. It is just that the second time through, we ignore the import failure. This does not matter, since Line
is not used at the module level. Therefore, the necessary namespace of surface
is loaded into path
. The namespace parsing of path
can therefore complete, permitting it to be loaded into surface
, completing the first encounter with from path import Line
. Thus the namespace parsing of surface
can proceed and complete, continuing on to whatever else may be necessary.
It is an easy and very clear idiom. The try: ... except ...
syntax clearly and succinctly documents the circular import issue, easing whatever future maintenance may be required. Use it whenever a refactor really is a bad idea.