Search code examples
pythonimporterror

Using common constants module leads to circular import


I want to import constants from a constants module from two different modules, but I get the following error:

Traceback (most recent call last):
  File "C:\Temp\tmp\pycircular\pycircular\pycircular.py", line 2, in <module>
    from my_classes.foo import Foo
  File "C:\Temp\tmp\pycircular\pycircular\my_classes\foo.py", line 1, in <module>
    from pycircular.constants import ANOTHER_CONSTANT
  File "C:\Temp\tmp\pycircular\pycircular\pycircular.py", line 2, in <module>
    from my_classes.foo import Foo
ImportError: cannot import name 'Foo' from partially initialized module 'my_classes.foo' (most likely due to a circular import) (C:\Temp\tmp\pycircular\pycircular\my_classes\foo.py)

My project structure is the following:

  |-constants.py
  |-my_classes
  |  |-foo.py
  |  |-__init__.py
  |-pycircular.py
  |-__init__.py
# =============
#     pycircular.py
# =============

from constants import SOME_CONSTANT
from my_classes.foo import Foo


def main():
    print(SOME_CONSTANT)
    my_foo = Foo()
    my_foo.do_something()


if __name__ == "__main__":
    main()
# =============
#     foo.py
# =============

from pycircular.constants import ANOTHER_CONSTANT


class Foo:

    def do_something(self):
        print(ANOTHER_CONSTANT)
# =============
#     constants.py
# =============

ANOTHER_CONSTANT = "ANOTHER"
SOME_CONSTANT = "CONSTANT"

I assume that it is the same problem as solved here https://stackoverflow.com/a/62303448/2021763. But I really do not get why from my_classes.foo import Foo in pycircular.py is called a second time.

Update:

After renaming the package pycircular to pycircular_pack it worked in PyCharm. But it only works because in Pycharm the option Add content roots to to PYTHONPATH is automatically set.

The output of sys.path is ['C:\\Temp\\tmp\\pycircular\\pycircular_pack', 'C:\\Temp\\tmp\\pycircular', 'C:\\Tools\\miniconda\\envs\\my_env\\python39.zip', 'C:\\Tools\\miniconda\\envs\\my_env\\DLLs', 'C:\\Tools\\miniconda\\envs\\my_env\\lib', 'C:\\Tools\\miniconda\\envs\\my_env', 'C:\\Tools\\miniconda\\envs\\my_env\\lib\\site-packages']

Without the option the output is ['C:\\Temp\\tmp\\pycircular\\pycircular_pack', 'C:\\Tools\\miniconda\\envs\\my_env\\python39.zip', 'C:\\Tools\\miniconda\\envs\\my_env\\DLLs', 'C:\\Tools\\miniconda\\envs\\my_env\\lib', 'C:\\Tools\\miniconda\\envs\\my_env', 'C:\\Tools\\miniconda\\envs\\my_env\\lib\\site-packages']

And without the option I only get it to work with absolute imports.

# pycircular.py

from constants import SOME_CONSTANT
from my_classes.foo import Foo

...
# foo.py

from constants import ANOTHER_CONSTANT

Solution

  • To elaborate based on the comments and edit:

    After renaming the package pycircular to pycircular_pack it worked in PyCharm. But it only works because in Pycharm the option Add content roots to to PYTHONPATH is automatically set.

    You should make sure the package directory is not set as a content root or source root. The directory hosting the package directory should be set as source root.

    C:\Temp\tmp\pycircular  # <- source root
    |- pycircular_pack  # <- not set as anything
    |  |- constants.py
    |  |- my_classes
    |  |  |- foo.py
    |  |  |- __init__.py
    |  |- pycircular.py
    |  |- __init__.py
    |- other_file.py  # <- for illustration's sake
    

    Now your sys.path will be set to include C:\Temp\tmp\pycircular only and there will be exactly one way to import things from your module.

    Namely,

    • other_file.py (outside the package) will be able to use the package as pycircular_pack
    • pycircular_pack/*.py can refer to modules in the pycircular_pack package by either
      • (e.g.) from .constants import ... (relative import from current package), or
      • (e.g.) from pycircular_pack.constants import ... (absolute import)
    • pycircular_pack/my_classes/*.py can refer to modules in the pycircular_pack package by either
      • (e.g.) from ..constants import ... (relative import from parent package), or
      • (e.g.) from pycircular_pack.constants import ... (absolute import)

    If your pycircular_pack package would contain a runnable script, e.g. a CLI as pycircular_pack/cli.py, then the correct way to run that script on the command line would be to use python -m pycircular_pack.cli; this has Python set up the path just like we want here, where python pycircular_pack/cli.py would not do the right thing.