pythonimportconfigurationrelative-import

Configuration file of Python app shared across sub-packages


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.


Solution

  • 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.

    Importing The File

    The Problem

    Assuming that you're importing one_kind_of_motor.py in main.py:

    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.

    A Multiple-Package Solution

    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.)

    A Single-Package Solution

    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
    

    Running The File

    The Problem

    Assuming that you're running one_kind_of_motor.py as a script:

    Note that __main__.__spec__ is always None 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__.

    A Solution

    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.