pythonpython-3.ximportpython-import

Reason for no relative imports in python scripts


I understand that in Python, relative imports are only allowed inside of packages. Personally, I find this functionality great; it makes it very easy to import stuff from other modules.

However, for files that are not inside a Package, relative imports are not allowed. A typical situation I encounter is in my small projects, which I often structure as follows :

my_project/
├── modules/
│   ├── __init__.py
│   ├── module1.py
│   └── module2.py
├── scripts/
    ├── script1.py
    └── script2.py

Now, from script1.py and script2.py, I'd want to import stuff from modules, but to do so I have to resort to convoluted ways, such as sys.path.append(Path(__file__).parent.parent.as_poxis()), which feel very weird.

It would feel very natural to just do from ..modules import whatever inside script1.py for example. I'm sure there is a good reason for why this isn't possible, but I am not sure why this is the case!

In summary, why are relative imports not allowed in Python scripts?


Solution

  • This is a general design decision. Full stop. I am not a core Python developper, so I can just try to guess the rationale behind it.

    It is indeed frustrating... until you understand that everything works smoothly as soon as you start to correctly package your projects. The rule is that something requires more than one file (and what you have shown is already complex enough...) it should be packaged according to the PyPA rules.

    Not only you immediately gain relative imports because the whole project is a package, but installing the project on a new system and have it immediately accessible becomes a piece of cake. You just declare any script as command line entry points, and they will end in the path immediately after installation. If you use high level tools to manage it (Pycharm or hatch or...) you also gain easy testing, including against a range of Python versions at no cost.

    That is the reason why I suspect that the underlying reason for not allowing relative imports outside packages is to encourage developpers to always package any project requiring more than one single script file.


    In order to be more explicit with the problems of script within a packaged project, I can provide you a direct link to the page about the initial configuration of the pyproject.toml file.

    Any function can be turned into an executable by declaring it in the project.scripts table.

    Your example could become:

    my_project/
    ├── pyproject.toml
    ├── my_project/
        ├── __init__.py
        ├── module1.py
        ├── module2.py
        ├── script1.py
        └── script2.py
    

    Then in your pyproject.toml you could add:

    [project.scripts]
    script1 = "my_project.script1:main"
    script2 = "my_project.script2:main"
    

    Installation would automatically generate the commands script1 and script2 that would automatically call the function main in respectively script1.py and script2.py.

    Learning how to package a project does require some work (even if a tool like hatch can give you a default project skeleton that you only have to edit) but in the long term, you will realize that it really was worth it.