When you work on semi-complex Python projects, they are sometimes composed out of several smaller projects. For example, you or your colleagues developed a library or package of classes and functions you now want to use in your current project. One way of including a package (here my_package
) into your current project is to copy it into your project folder (or have it as a git submodule). A project structure could look like this, where the package(s) are all included inside the libs
folder:
1
2
3
4
5
6
7
$workspaceFolder
└── my_code
├── libs
│ └── my_package
│ ├── __init__.py
│ └── classes.py
└── main.py
The classes.py
may contain the following code (__init__.py
is empty):
1
2
3
4
5
6
class MyClass:
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
and main.py
can access MyClass
by importing it with its absolute path:
1
2
3
4
5
from libs.my_package.classes import MyClass
if __name__ == "__main__":
a = MyClass("a")
print(a)
However, sometimes your projects require that a libs
directory is on the same folder level as the folder where your project can be found, or several projects share the same packages and they are stored outside of your workspace. Then the project structure could look like this:
1
2
3
4
5
6
7
$workspaceFolder
├── libs
│ └── my_package
│ ├── __init__.py
│ └── classes.py
└── my_code
└── main.py
Now main.py
can’t access MyClass
using the import statement from above because libs/my_package
is not in the folder that main.py
is in, and when running main.py
, the following ModuleNotFoundError
is raised:
1
2
3
4
Traceback (most recent call last):
File "$workspaceFolder\my_code\main.py", line 1, in <module>
from libs.my_package.classes import MyClass
ModuleNotFoundError: No module named 'libs.my_package'
There is a dirty fix to remove the ModuleNotFoundError
by extending the PYTHONPATH
inside of main.py
. PYTHONPATH
is an environment variable that holds paths to additional directories in which the Python interpreter will look into to find packages and modules. PYTHONPATH
can be manually extended within a Python file using sys
:
1
2
3
4
5
6
7
8
import sys
sys.path.append("../libs")
from my_package.classes import MyClass
if __name__ == "__main__":
a = MyClass("a")
print(a)
However, this solution not only looks terrible, but it also has a lousy code design. Imagine you got several Python files that want to access MyClass
. In each of these files, you have to add the first two lines. When you now move libs
somewhere else. Every file that imports MyClass
has to be changed, which is tedious and error-prone. A better solution would be to have a single file to extend the PYTHONPATH
. You can either extend PYTHONPATH
systemwide by appending the full path to libs
to it, whereas several paths are separated using a colon :
. But then you have to tell everyone who uses your code to do this for their system. So the preferred solution is to ask VSCode to extend the PYTHONPATH
only for your project which you can also add to your git repository such that others don’t have to extend their PYTHONPATH
manually.
First, you need to add a launch.json
to your workspace that tells VSCode what and how to run your code. To create a launch.json
, go to Run and Debug in the VSCode sidebar by clicking on the bug and run icon or pressing Ctrl+Shift+D.
Then click on create launch.json file and choose Module, press Enter, and enter the path to the Python file you would like to run while folders a separated with a dot .
. For the workspace in this example, you would enter my_code.main
because main.py
is inside my_code
, which is the workspace’s root. Now VSCode added a .vscode
directory to your workspace, and inside it, you can find a launch.json
file. A launch.json
allows you to run your code regardless of which files are currently opened or in focus. You can now run your code by pressing Ctrl+F5 or Cmd+F5.
Inside the launch.json
you have to add a new env
segment that will tell VSCode to extend the PYTHONPATH
before running your program:
1
2
3
4
5
6
7
8
9
10
11
12
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Module",
"type": "python",
"request": "launch",
"module": "my_code.main",
"env": {"PYTHONPATH": "${workspaceFolder}/libs/"}
}
]
}
For this example, PYTHONPATH
will be extended with ${workspaceFolder}/libs/
. ${workspaceFolder}
is the variable that contains the path to the root folder of your current VSCode workspace, and as libs
is a folder inside the root /libs/
is added. You can also use relative paths, including ..
when libs
is outside your workspace.
Now the sys.path.append
can be removed from main.py
and you can run main.py
by pressing Ctrl+F5/Cmd+F5. But now VSCode complains that it can’t find my_package.classes
and it won’t give you auto-completion for any of the classes and functions from my_package
, and having not auto-completion almost defeats the whole purpose of having an editor or IDE.
However, you can get auto-completion back by adding a .vscode/settings.json
to your workspace. To do so, open the command palette by pressing Ctrl+Shift+P on Windows and Linux or Cmd+Shift+P on macOS and enter settings.json and press Enter when Preferences: Open Workspace Settings (JSON) is selected.
This will create a settings.json
within .vscode,
and in it, you have to tell VScode where to look for additional packages by adding a python.analysis.extraPaths
segment containing the path to libs
:
1
2
3
{
"python.analysis.extraPaths": ["${workspaceFolder}/libs/"]
}
This will extend the PYTHONPATH
for the VSCode code analysis and auto-completion, and VSCode won’t complain about an import error anymore:
I hope this article was helpful for you, and you don’t have to work with messy relative imports in Python anymore. If you have any questions about this article, feel free to join our Discord community to ask them over there. And if you want to know how to install and run multiple Python versions under Windows 10/11 check out this article and video: