0

I am an experienced java enterprise developer but very new to python enterprise development shop. I am currently, struggling to understand why some imports work while others don't.

Some background: Our dev team recently upgraded python from 3.6 to 3.10.5 and following is our package structure

src/
bunch of files (dockerfile, Pipfile, requrirements.txt, shell scripts, etc)
  package/
      __init__.py
      moduleA.py
      subpackage1/
          __init__.py
          moduleX.py
          moduleY.py
      subpackage2/
          __init__.py
          moduleZ.py
      tests/
          __init__.py
          test1.py
    

Now, inside the moduleA.py, I am trying to import subpackage2/moduleZ.py like so

from .subpackage2 import moduleZ

But, I get the error saying

ImportError: attempted relative import with no known parent package

The funny thing is that if I move moduleA.py out of package/ and into src/ then it is able to find everything. I am not sure why is this the case.

I run the moduleA.py by executiong python package/moduleA.py.

Now, I read that maybe there is a problem becasue you have you give a -m parameter if running a module as a script (or something on those lines). But, if I do that, I get the following error:

ModuleNotFoundError: No module names 'package/moduleA.py'

I even try running package1/moduleA and remove the .py, but that does not work either. I can understand why as I technically never installed it ?

All of this happened because apparently, the tests broke and to make it work they added relative imports. They changed the import from "from subpackage2 import moduleZ" to "from .subpackage2 import moduleZ" and the tests started working, but the app started failing.

Any understanding I can get would be much appreciated.

daze-hash
  • 31
  • 7
  • Did you install package? – juanpa.arrivillaga Nov 30 '22 at 16:43
  • You describe your directory structure as containing `package`, but your uses are all using `package1`. Please fix it up so the two agree (I'm assuming they're consistent locally and you just typoed one of them here). – ShadowRanger Nov 30 '22 at 16:48
  • @juanpa.arrivillaga: They already said that they have not installed it: "I can understand why as I technically never installed it ?" It can still work, if `sys.path` includes the `src` directory (e.g. because it's the working directory) and they use the `-m` module of launching the script correctly. – ShadowRanger Nov 30 '22 at 16:53
  • 1
    Are you running from this directory or are you installing the package? Is there a setup.py describing how to package and install? – tdelaney Nov 30 '22 at 16:53
  • @ShadowRanger My script that starts the app is in the package folder, which then invokes the modules in package1 – daze-hash Nov 30 '22 at 19:02
  • @tdelaney I am running from the src/ directory. I do have a setup.py in the src directory. And the only thing it has is this : from setuptools import setup setup( setup_requires=["pbr>=1.9", "setuptools>=30.0.0", "pytest-runner", "tox"], pbr=True ) – daze-hash Nov 30 '22 at 19:04
  • @daze-hash: There is no `package1` in your directory tree. You have `src`, which contains `package`, which contains `subpackage1` and `subpackage2`, but no `package1` anywhere. – ShadowRanger Nov 30 '22 at 19:16
  • @ShadowRanger You're right. Typo. Updated it. Sorry about that – daze-hash Nov 30 '22 at 19:18
  • 1
    The best solution is to update that setup.py to do the full install. Then you have options like installing to the machine, to a python virtual environment on the machine, or even a "develop" mode where python points itself to your source treee. – tdelaney Nov 30 '22 at 19:20

2 Answers2

2

The -m parameter is used with the import name, not the path. So you'd use python3 -m package.moduleA (with . instead of /, and no .py), not python3 -m package/moduleA.py.

That said, it only works if package.moduleA is locatable from one of the roots in sys.path. Shy of installing the package, the simplest way to make it work is to ensure your working directory is src (so package exists in the working directory):

$ cd path/to/src
$ python3 -m package.moduleA

and, with your existing setup, if moduleA.py includes a from .subpackage2 import moduleZ, the import should work; Python knows package.moduleA is a module within package, so it can use a relative import to look for a sibling package to moduleA named subpackage2, and then inside it it can find moduleZ.

Obviously, this is brittle (it only works if you cd to the src root directory before running Python, or hack the path to src in PYTHONPATH, which is terrible hack if the code ever has to be run by anyone else); ideally you make this an installable package, install it (in global site-packages, user site-packages, or within a virtual environment created with the built-in venv module or the third-party virtualenv module), and then your working directory no longer matters (since the site-packages will be part of your sys.path automatically). For simple testing, as long as the working directory is correct (not sure what it was for you), and you use -m correctly (you were using it incorrectly), relative imports will work, but it's not the long term solution.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • So, I am trying to run this inside the docker container. Therefore, my working directory is src/. But, I will try to add the PYTHONPATH as path/to/src and then run with -m and see what happens. – daze-hash Nov 30 '22 at 19:08
  • Nope. Now I have this "Traceback (most recent call last): File "/opt/python/lib/python3.10/site-packages/importlib_metadata/__init__.py", line 564, in from_name return next(cls.discover(name=name)) StopIteration " – daze-hash Nov 30 '22 at 19:16
  • And, importlib_metadata.PackageNotFoundError: No package metadata was found – daze-hash Nov 30 '22 at 19:16
  • 1
    @daze-hash: That exception is *weird*, and almost certainly unrelated to what you've shown us. Guessing you have some weird import hooks installed (possibly without realizing it). In any event, it doesn't matter if this is being run in a docker container, it should be made an installable package that you install within the docker container, so the working directory stops mattering (and `python3 -m package1.moduleA` works without even needing to know where exactly the package was installed). – ShadowRanger Nov 30 '22 at 19:23
  • 2
    @daze-hash: Also, a warning: `PYTHONPATH` is a *hack*. Your code should not require anything of uses but to run `pip3 install path/to/packagename-x.y.z.tar.gz` (or if published to a PyPI server, just `pip3 install packagename`). *Anything* aside from making an installable package and installing it is a hack. Just write a simple `setup.py`, run `python3 setup.py sdist` to make a source distribution, and then install that source distribution within your container. – ShadowRanger Nov 30 '22 at 19:24
  • That is what @tdelaney mentioned as well. I think I will go with that then. Thank you so much. How do I go about accepting an answer here ? – daze-hash Nov 30 '22 at 19:28
  • @daze-hash: There's a check mark immediately under the voting arrows near the upper left side of each answer. Clicking the check mark accepts the answer. – ShadowRanger Nov 30 '22 at 19:33
  • @daze-hash: One additional note: You could simplify matters even further by using the `entry_points` argument to `setup` in `setup.py` to have it generate top-level invokers of the module that get installed in the `bin` directory for you, e.g. `setup(...otherargs..., entry_points={'console_scripts': ['moduleA=package.moduleA:main']})` (assuming `main` is the name of the top-level function that implements the main-like behaviors), so, when installed in docker, you can just run `moduleA` and it will find it in the `PATH` and invoke it as if you imported it and called `main` in it. – ShadowRanger Nov 30 '22 at 20:02
0

So first of all - the root importing directory is the directory from which you're running the main script.

This directory by default is the root for all imports from all scripts.

So if you're executing script from directory src you can do such imports:

from package.moduleA import *
from package.subpackage1.moduleX import *

But now in files moduleA and moduleX you need to make imports based on root folder. If you want to import something from module moduleY inside moduleX you need to do:

# this is inside moduleX
from package.subpackage1.moduleY import *

This is because python is looking for modules in specific locations. First location is your root directory - directory from which you execute your main script. Second location is directory with modules installed by PIP. You can check all directories using following:

import sys
for p in sys.path:
    print(p)

Now to solve your problem there are couple solutions. The fast one but IMHO not the best one is to add all paths with submodules to sys.path - list variable with all directories where python is looking for modules.

new_path = "/path/to/application/app/folder/src/package/subpackage1"
if new_path not in sys.path:
    sys.path.append(new_path)

Another solution is to use full path for imports in all package modules:

from package.subpackage1.moduleX import *

I think in your case it will be the correct solution.

You can also combine 2 solutions. First add folders with subpackages to sys.path and use subpackage folders as a root folders for imports. But it's good solution only if you have complex submodule structure. And it's not the best solution if in future you will need to deploy your package as a wheel or share between multiple projects.

  • Well, what I fail to understand is that if I pull my moduleA.py out of package/ and into src/ and then run it, things seem to run okay. Why would that happen ? Also, everything also works if I remove the "." from the import statement. – daze-hash Nov 30 '22 at 19:06
  • 1
    The OP asks "How do I make relative imports work" and your answer is "Apply a bunch of hacks so that your package isn't a single package with subpackages and submodules, just a top-level packages/modules, so you can use absolute imports". This is a *terrible* idea; among other issues, it will mean that importing `package.subpackage1.moduleX` and `subpackage1.moduleX` both work, but they'll be *different* modules; a class is defined `moduleX.py` will have two distinct definitions; `isinstance` checks for an instance created from one import, against the class from the other, will return `False`. – ShadowRanger Nov 30 '22 at 19:31
  • @ShadowRanger relative imports is a terrible idea. And those are not hacks, it's how modules structure works. – Ivan Perehiniak Nov 30 '22 at 19:36
  • 1
    @IvanPerehiniak: They really aren't. The whole point of relative imports is that they allow you to perform imports from within your own package tree without hard-coding knowledge of the *entire* hierarchy into the tree (so a refactor requires a smaller, possibly zero, number of changes to the imports). *Implicit* relative imports were bad (in the Py2 days, if you had a package, `foo` with submodules `foo.main` and `foo.math`, and `foo.main` did `import math`, it would get `foo.math`, not the global `math` module), but *explicit* relative imports are perfectly fine. – ShadowRanger Nov 30 '22 at 19:41
  • @ShadowRanger https://stackoverflow.com/questions/4209641/absolute-vs-explicit-relative-import-of-python-module – Ivan Perehiniak Nov 30 '22 at 19:43
  • The problem with (at least one of) your proposed solution is that, if you add multiple directories within the package to `sys.path`, then you're recreating the problem Python 2 implicit relative imports had (that `from __future__ import absolute_import` avoids on Py2, and Py3 prevents by default). Now if `package.subpackage1` is actually `foo.math`, you've put a new top-level `math` package in the path, shadowing the built-in `math`. `foo.math` is a perfectly reasonable name to claim, `math` is not. – ShadowRanger Nov 30 '22 at 19:43
  • @IvanPerehiniak: Yep, you're making the mistake I expected. All the arguments against relative imports are due to the problem with *implicit* relative imports (a thing that doesn't even exist in Python 3). Your own link notes that with `from __future__ import absolute_import` in effect on Py 2, and on Py 3, implicit relative imports aren't possible. Explicit relative imports (`from .x import y`) are fine, since they don't overlap absolute imports. In your zeal to avoid safe explicit relative imports, you reinvented the worst problems of implicit relative imports. – ShadowRanger Nov 30 '22 at 19:46
  • @ShadowRanger Adding folder to asys.path is only example how it can be done. I've recommended to use absolute import. – Ivan Perehiniak Nov 30 '22 at 19:46
  • @ShadowRanger Relative imports is one of the types of imports in python. If you like to use it it's ok. But the recommended approach is to use absolute imports. If you've learned how to use relative imports and you use it all the time it does not mean it's better for everyone. Just google it and attach a link where people say that relative imports are better. – Ivan Perehiniak Nov 30 '22 at 19:58
  • 1
    @IvanPerehiniak: I'm not saying they're better. I'm saying you are *100%* wrong when you say "the recommended approach is to use absolute imports" because you don't know understand the recommendations you've read. Those recommendations, from the Python 2 days, are to use `absolute_import`, as in `from __future__ import absolute_import`, to disable *implicit* relative imports. The recommendations are, and always have been, neutral on whether it's better to use explicit relative imports over absolute imports, and the OP's question was specifically on making relative imports work. – ShadowRanger Nov 30 '22 at 20:10
  • @ShadowRanger Yes, that's probably why author has problem with relative import and not absolute. I'm sure if he replace relative imports with absolute - it will work. – Ivan Perehiniak Nov 30 '22 at 20:17
  • PEP 8: "Absolute imports are recommended, as they are usually more readable and tend to be better behaved (or at least give better error messages) if the import system is incorrectly configured (such as when a directory inside a package ends up on sys.path)". "Standard library code should avoid complex package layouts and always use absolute imports." Any arguments? – Ivan Perehiniak Nov 30 '22 at 20:28