I am building an app to control some hardware. I have different kinds of hardware that I implemented in packages: motors and measurement devices. My file structure is as follow:
name_of_my_app/
__init__.py
main.py
config.ini
CONFIG.py
motors/
__init__.py
one_kind_of_motor.py
measurement_devices/
__init__.py
one_kind_of_measurement_device.py
I would like to have a single configuration file (ini) to store settings of the motors and measurement devices.
I started with the answer from u/symmitchry on that reddit post: https://www.reddit.com/r/learnpython/comments/2hjxk5/whats_the_proper_way_to_use_configparser_across/ as it seemed the most sensible thing to do. From what I understand, I could have a config.ini
at the root of my app together with a CONFIG.py
. CONFIG.py
would take care of reading the settings from config.ini
and make them available through an import of CONFIG.py
. This works when I import the configuration from main.py
. However, when I try to import the configuration from one_kind_of_motor.py
with:
from ... import CONFIG
as suggested by @f3lix in this post: Importing modules from parent folder, I get a:
ImportError: attempted relative import with no known parent package
That is when I added the __init__.py
at the root hoping it would solve my issue, but it didn't. I also tried with ..
and .
instead of ...
and the only thing it did was to show how little I understand about this topic... (it keeps throwing the same error above).
Currently, to work around this problem, I read the specific settings I need from config.ini
in every file. But I know it is sub-optimal.
I program in Python like a scientist, i.e. I don't have a software developer background and I normally use basic Python, but I'd like to learn clean ways of disseminating my research. I've seen a number of posts about similar questions (and you can call me out for this), but I failed to understand their answer. Can you please explain this to me in Layman's term?
EDIT: In response to the thorough answer from @ImpeccableChicken, I created a simplified file-structure and uploaded it on GitHub to demonstrate the problem. Using
from .. import CONFIG
produces an error
ImportError: attempted relative import with no known parent package
I am using a Windows machine and I execute my scripts from Visual Code.
This answer, together with its top comments, explains relative imports quite well. Crucially, for relative imports, the dots indicate that the import system should move up in the package hierarchy, not in the filesystem tree.
Assuming that you're importing one_kind_of_motor.py
in main.py
:
python main.py
adds the parent directory of main.py
to sys.path
(i.e. name_of_my_app/
will be searched for packages and modules).__main__
module (corresponding to the main.py
file), import motors.one_kind_of_motor
creates the package motors
with submodule motors.one_kind_of_motor
. The package motors
has no parent package; this is apparent from the fact that its name contains no dots. The module motors.one_kind_of_motor
has motors
as its parent.motors.one_kind_of_motor
, from .. import CONFIG
indicates that the import system should move up two levels in the package hierarchy. The first .
corresponds to the package motors
, which is the parent of the current module. The second .
corresponds to the parent of motors
, but motors
has no parent package, resulting in an error.It might be helpful to remember that, in most cases, the number of leading dots in a relative-import statement cannot exceed the number of dots in the name of the module containing that statement.
(Although this guideline is quite helpful, it is not true in the general case; see __package__
and __spec__
, for example.)
One important thing to note is that the root cause of the problem is not the relative-import statement in one_kind_of_motor.py
, but rather the way in which the module is imported in main.py
. The relative import in one_kind_of_motor.py
fails because one_kind_of_motor.py
is imported as motors.one_kind_of_motor
in another file.
If you are only ever going to run top-level scripts (i.e. scripts directly in name_of_my_app/
), you can import the configuration from one_kind_of_motor.py
with
import CONFIG
When running python main.py
, the import succeeds because name_of_my_app/
is in sys.path
.
(For this solution, the __init__.py
file in name_of_my_app/
is not necessary.)
If you want the relative import to work, you can avoid the error by ensuring that all files intended for importing are nested in a directory adjacent to the scripts. For example, you can arrange your project as follows:
name_of_my_app/
main.py
devices/
__init__.py
config.ini
CONFIG.py
motors/
__init__.py
one_kind_of_motor.py
measurement_devices/
__init__.py
one_kind_of_measurement_device.py
You can then adjust any imports in main.py
accordingly. With this structure, you can import the configuration from one_kind_of_motor.py
with
from .. import CONFIG
When running python main.py
, one_kind_of_motor.py
now corresponds to the module devices.motors.one_kind_of_motor
, and the relative-import statement is equivalent to
from devices import CONFIG
Assuming that you're running one_kind_of_motor.py
as a script:
python one_kind_of_motor.py
adds the parent directory of one_kind_of_motor.py
to sys.path
(i.e. name_of_my_app/motors/
will be searched for packages and modules).__main__
module (corresponding to the one_kind_of_motor.py
file), from .. import CONFIG
indicates that the import system should move up two levels in the package hierarchy. The first .
corresponds to the parent of __main__
. In this case, the import system cannot find the parent package of __main__
, since __main__.__spec__
is None
. This behavior is explained in the Python documentation:Note that
__main__.__spec__
is alwaysNone
in the last case, even if the file could technically be imported directly as a module instead. Use the -m switch if valid module metadata is desired in__main__
.
To avoid the error, you can invoke the python
command with the -m switch and supply the module name. Specifically, adjust the import statements and project structure as described in one of the solutions above, and ensure that you are in the name_of_my_app/
directory. Then, for the multiple-package case, run
python -m motors.one_kind_of_motor
or, for the single-package case, run
python -m devices.motors.one_kind_of_motor
Doing this will ensure that the __main__
module (corresponding to the one_kind_of_motor.py
file) contains valid module metadata (as explained here).
However, the documentation mentions an important caveat associated with this approach:
Note also that even when
__main__
corresponds with an importable module and__main__.__spec__
is set accordingly, they’re still considered distinct modules.
A consequence of this is that, if one_kind_of_motor.py
is part of an import cycle (i.e. running one_kind_of_motor.py
indirectly causes itself to be imported), you would end up with two copies of all classes and functions defined in one_kind_of_motor.py
. If an import cycle is necessary (avoid it if you can), you can define an entry-point function in one_kind_of_motor.py
and call this function from main.py
or another top-level script. That is, in one_kind_of_motor.py
, create a function that contains the desired entry-point code:
# Name this whatever makes sense
def main():
...
Then, somewhere in main.py
, or in another script in name_of_my_app/
, call this function:
import devices.motors.one_kind_of_motor
...
# Call the entry point
# Alternatively, call this only for certain script arguments or other conditions
devices.motors.one_kind_of_motor.main()
Now, rather than running one_kind_of_motor.py
directly, run the top-level script that contains the import and entry-point call.