Suppose we have two modules with cyclic dependencies:
# a.py
import b
def f(): return b.y
x = 42
# b.py
import a
def g(): return a.x
y = 43
The two modules are in the directory pkg with an empty __init__.py. Importing pkg.a or pkg.b works fine, as explained in this answer. If I change the imports to relative imports
from . import b
I get an ImportError when trying to import one of the modules:
>>> import pkg.a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "pkg/a.py", line 1, in <module>
from . import b
File "pkg/b.py", line 1, in <module>
from . import a
ImportError: cannot import name a
Why do I get this error? Isn’t the situation pretty much the same as above? (Is this related to this question?)
Edit: I’m aware of ways to avoid the circular dependency, but I’m interested in the reason for the error anyway.
First let’s start with how
from importwork in python:Well first let’s look at the byte code:
hmm interesting :), so
from foo import baris translated to firstIMPORT_NAME foowhich equivalent toimport fooand thenIMPORT_FROM bar.Now what
IMPORT_FROMdo ?let’s see what python do when he found
IMPORT_FROM:Well basically he get the the names to import from, which is in our
foo()function will bebar, then he pop from the frame stack the valuevwhich is the return of the the last opcode executed which isIMPORT_NAME, then call the functionimport_from()with this two arguments :As you can see the
import_from()function is quiet easy, it try first to get the attributenamefrom the modulev, if it don’t exist it raiseImportErrorelse return this attribute.Now what this have to do with relative import ?
Well relative import like
from . import bare equivalent for example in the case that is in the OP question tofrom pkg import b.But how this happen ? To understand this we should take a look to the
import.cmodule of python specially to the function get_parent(). As you see the function is quiet long to list here but in general what it does when it see a relative import is to try to replace the dot.with the parent package depending on the__main__module, which is again from the OP question is the packagepkg.Now let’s put all this together and try to figure out why we end up with the behavior in the OP question.
For this it will help us if we can see what python do when doing imports, well it’s our lucky day python come already with this feature which can be enabled by running it in extra verbose mode
-vv.So using the command line:
python -vv -c 'import pkg.b':hmm what just happen before the
ImportError?First)
from . import ainpkg/b.pyis called, which is translated as explained above tofrom pkg import a, which is again in bytecode is equivalent toimport pkg; getattr(pkg, 'a'). But wait a minuteais a module too ?!Well here came the fun part if we have something like
from module|package import modulein this case a second import will happen which is the import of the module in the import clause. So again in the OP example we need now to importpkg/a.py, and as you know first of all we set in oursys.modulesa key for our new module which will bepkg.aand then we continue our interpretation of the modulepkg/a.py, but before the modulepkg/a.pyfinish importing it callfrom . import b.Now come the Second) part,
pkg/b.pywill be imported and in it turn it will first attempt toimport pkgwhich becausepkgis already imported so there is a keypkgin oursys.modulesit will just return the value of that key. Then it willimport bset thepkg.bkey insys.modulesand start the interpretation. And we arrive to this linefrom . import a!But remember
pkg/a.pyis already imported which mean('pkg.a' in sys.modules) == Trueso the import will be skipped, and only thegetattr(pkg, 'a')will be called , but what will happen ? python didn’t finish importingpkg/a.py!? So onlygetattr(pkg, 'a')will be called , and this will raise anAttributeErrorin theimport_from()function, which will be translated toImportError(cannot import name a).DISCLAIM : This is my own effort to understand what is happening inside the interpreter, i’m far away of being an expert.
EDIt: This answer was rephrased because when i tried to read it again i remarked how my answer was bad formulated, hope now it will be more useful 🙂