Home Random Python tips #1: Importing from a parent or sibling folder
Post
Cancel

Random Python tips #1: Importing from a parent or sibling folder

The problem

Suppose that you have a script script.py that imports something from another file dependency.py. The two files are at the same level in the project hierarchy.

1
2
3
my_project/
├── script.py
└── dependency.py

To import whatever you need from dependency.py, you can just use an absolute import statement, like so:

1
from dependency import my_function

So far so good. But now let’s imagine the project has grown a bit and you need to put script.py into a subfolder, whereas dependency.py needs to stay at the top-level because other files depend on it.

1
2
3
4
my_project/
├── subfolder
│   └── script.py
└── dependency.py

If you try to run the script from the root folder, you’ll be greeted with this:

1
2
3
4
5
$ python subfolder/script.py
Traceback (most recent call last):
  File "/home/robin/tmp/python_module_demo/my_project/subfolder/script.py", line 1, in <module>
    from dependency import my_function
ModuleNotFoundError: No module named 'dependency

Ok, Python cannot find the dependency module anymore. Maybe we need to tell it to fetch it one folder higher ? Let’s try changing from dependency import my_function to from ..dependency import my_function.

1
2
3
4
5
$ python subfolder/script.py
Traceback (most recent call last):
  File "/home/robin/tmp/python_module_demo/my_project/subfolder/script.py", line 1, in <module>
    from ..dependency import my_function
ImportError: attempted relative import with no known parent package

Still doesn’t work, but the error is different. You can try running script.py from subfolder/ instead, but you’ll get similar errors.

The solution

Every Python programmer has been confronted to that issue. Curiously, Googling “python import from parent folder” will almost always point you toward weird hacks that involve modifying sys.path, setting a PYTHONPATH variable, or even using pip to link the current directory as a development package. There is a much more idiomatic and (in my opinion) much more practical solution.

Simply put all your code in a subfolder (which we’ll call my_module because that’s actually what it is), like so:

1
2
3
4
5
my_project/
└ my_module
  ├── subfolder
  │   └── script.py
  └── dependency.py

For the import statement, use from ..dependency import my_function. The .. means “one level higher from subfolder, which correspond to my_module.

So how are you supposed to run script.py now ? The trick is to tell Python not to run it as a script, but as a Python module. You can do this using the -m parameter, like so:

1
python -m my_module.subfolder.script

Now, because all the code is encapsulated inside a single module, Python will have no issue fetching my_function from one file up. Note that the working directory is still the one where you invoke the command from, so my_project/ in this case.

This solution also solves the problem of imports from sibling folders. Let’s say for example that you have the following project structure:

1
2
3
4
5
my_project/
├── subfolder_A
│   └── script_A.py
└── subfolder_B
    └── dependency_B.py

Where script_A.py wants to import my_function from dependency_B.py. Just put everything in a Python module like before:

1
2
3
4
5
6
my_project/
└ my_module
  ├── subfolder_A
  │   └── script_A.py
  └── subfolder_B
      └── dependency_B.py

Now script_A.py can import the function like this: from ..subfolder_B.dependency_B import my_function, and you can run it with

1
python -m my_module.subfolder_A.script_A

Additional tricks

The main guard

In both the examples above, our target script is also itself a module, and therefore it’s possible to import stuff from it. However, when a module is imported, everything in its global scope is executed, which can have unintended consequences. In other words, if your script.py looks like this:

1
2
3
4
def my_other_function():
    print("Hello from my_other_function")
    
print("Hello")

Then importing from script import my_other_function in another file will also execute the print("Hello") statement. This is why Python scripts traditionally wrap their code in a if __name__ == "__main__": statement:

1
2
3
4
5
def my_other_function():
    print("Hello from my_other_function")
    
if __name__ == "__main__":
    print("Hello")

This way, print("Hello") will only execute if the module is ran directly with python -m, and not if script is imported by some other module.

Run a whole folder as a script

This is a small but neat feature. In the first example above, if you rename script.py as __main__.py, you will be able to call subfolder directly as if it was an executable script itself: python -m my_module.subfolder. It can be pretty useful to get rid of superflous submodules and shorten the invocation path.

This post is licensed under CC BY 4.0 by the author.