To understand circular dependencies, you need to remember that Python is essentially a scripting language. Execution of statements outside methods occurs at compile time. Import statements are executed just like method calls, and you must think of them as method calls to understand them.
When you import, what happens depends on whether the file you are importing exists in the module table. If so, Python uses what is currently in the character table. If not, Python starts reading the module file, compiling / executing / importing whatever it finds. The characters referenced at compile time are found or not, depending on whether they were viewed or not yet seen by the compiler.
Imagine you have two source files:
X.py file
def X1: return "x1" from Y import Y2 def X2: return "x2"
Y.py file
def Y1: return "y1" from X import X1 def Y2: return "y2"
Now suppose you compile an X.py file. The compiler starts by defining the X1 method, and then removes the import statement in X.py. This causes the compiler to pause X.py compilation and start compiling Y.py. Soon after, the compiler enters the import statement in Y.py. Since X.py is already in the module table, Python uses the existing incomplete X.py character table to satisfy any requested links. Any characters that appear before the import statement in X.py are now in the character table, but there are no characters after that. Since X1 now appears before the import statement, it is successfully imported. Python then resumes compiling Y.py. In doing so, it defines Y2 and completes the compilation of Y.py. He then resumes compiling X.py and finds Y2 in the Y.py character table. Compilation ultimately ends without error.
Something completely different happens if you try to compile Y.py from the command line. When compiling Y.py, the compiler enters the import statement before it defines Y2. He then begins compiling X.py. Soon, he gets into the import statement in X.py, which requires Y2. But Y2 is undefined, so compilation fails.
Note that if you change X.py to import Y1, compilation will always succeed, no matter which file you compile. However, if you modify the Y.py file to import the X2 character, no file will be compiled.
At any time when module X or any module imported by X can import the current module, DO NOT use:
from X import Y
Every time you think that there may be circular imports, you should also avoid referencing variables in other modules. Consider the innocent looking code:
import X z = XY
Suppose a module X imports this module before this module imports X. Next, suppose that Y is defined in X after the import statement. Then Y will not be determined when this module is imported, and you will get a compilation error. If this module first imports Y, you can handle it. But when one of your employees innocently redefines the definitions in the third module, the code will break.
In some cases, you can resolve circular dependencies by moving the import statement below the characters needed for other modules. In the above examples, definitions are never broken before import. Definitions after the import statement sometimes fail, depending on the compilation order. You can even put import instructions at the end of the file if none of the imported characters are required at compile time.
Note that moving import statements in a module hides what you are doing. Compensate for this with a comment at the top of your module like this:
In general, this is bad practice, but sometimes it is difficult to avoid.