Relative imports in python
Check out the follow on post on the best way to debug in vscode considering the way the imports system works
If you have spent hours trying to fully understand the way python does imports after getting (unhelpful) errors like ModuleNotFoundError: No module named <your module>
or ImportError: attempted relative import with no known parent package
and have been banging your head against the truly awful python docs, then look no further.
I’ll explain why relative imports don’t work when you run a file directly (python somefile.py
) and how the import system works in general. It’s less convoluted than the docs make it out to be. This will be just the critical facts without the blabbing. Estimated reading time: 15 minutes.
Module vs Package:
Module
- Usually means a
.py
file but, confusingly, can also refer to a package.
Package
- A module that contains other modules. E.g.: A folder with an
__init__.py
and.py
files.
Import procedure:
When you import
, python will search for the module in the following way:
1. Convert relative imports to absolute
Relative imports have dots at the start of the path, absolute imports do not. Absolute: import somepackage
. Relative: import .sibling
.
You might have done this before:
In /folder/pyfile.py
you have the line from siblingfile import somefunc
, and then you ran: python pyfile.py
, and it worked.
folder/
├── __init__.py
├── pyfile.py
└── siblingfile.py
You did an absolute import of a module. Python knew where to look because the folder containing pyfile.py
and siblingfile.py
was automatically added to the module search paths when you ran python pyfile.py
.
Python needs absolute paths behind the scenes so relative import paths need to be converted to absolute. It will use a variable called __package__
(more detail further on) to do this. This is the exact function that does this in the source code (the package
parameter comes from the __package__
variable).
All this probably isn’t fully clear at the moment but I invite you to retain your curiosity as I’ll flesh out the explanation further down.
2. Cache check
After converting the path from relative to absolute (if necessary) python will check if the module is already imported by looking at the already imported module cache1. If it’s there, it does nothing.
3. Lookup list of paths
If not in the cache it will try to find the module by prepending various paths to the import path. E.g. for import somepackage.subpackage.somecode
it will add the path C:\Python311\Lib\
and check if the folder C:\Python311\Lib\somepackage\subpackage
exists and whether a folder somecode\
or a file somecode.py
is in it. If not it will keep searching by trying other paths. It’s really that basic.
This list of paths it checks can be seen if you run this command: import sys
then print(sys.path)
2. For me on windows the output is:
- ‘C:\Python311\Lib’,
- ‘C:\Python311’,
- ‘C:\Python311\Lib\site-packages’,
- C:\Users\me\Desktop\mypackage’, (this is critical)
Note the last line above:
I ran:
python.exe C:\Users\me\Desktop\mypackage\main.py
and when you do this python adds the current folder to the lookup path list.So:
C:\Users\me\Desktop\mypackage\
was added and all python files and subfolders in that folder are now available for import.
However if I run a file in a subfolder:python.exe C:\Users\me\Desktop\mypackage\subfolder1\module1.py.py
- Then only:
C:\Users\me\Desktop\mypackage\subfolder1\
is added to the lookup list (and only its files and subfolders are searched—apart from the default paths).
The issue: In module1.py
(below) if I try to import from module2.py
there will be an error as python only knows about what’s in subfolder1
.
mypackage/
├── __init__.py
├── main.py
├── subfolder1/
│ ├── __init__.py
│ └── module1.py
└── subfolder2/
├── __init__.py
└── module2.py
4. Execute __init__.py
files
Any __init__.py
files along the import path and the module being imported will be executed. E.g. from subpackage.somefile import somefunc
will execute:
subpackage\__init__.py
if it exists, and:subpackage.py
3
Back to relative imports…
Where relative imports work
Doesn’t work:
python somefile.py
(AKA “running directly”4)python -m somefile
(where “somefile” is a file not a folder)
Works:
python -m somefolder
import somefile
orimport somefolder
- Relative imports also work in
somefile
andsomefolder
when you import them in another package (NB: they have to be findable in the lookup path).
- Relative imports also work in
Behind the scenes python does a simple check to see if __package__ == None
. When running a file directly: python.exe somefile.py
or: python.exe -m somefile
, __package__
is set by python to None
. And that’s why relative imports in that situation won’t work. Check out the source code if you don’t believe me. There’s no real technical reason why relative imports can’t work everywhere but I suppose a lot of code would have to be changed. The exact code for this check is here.
A relative import will work (depending on folder level) when calling python.exe -m <somefolder path>
because the module’s __package__
variable is set to <somefolder path>
. Python will check the import strings in a file, e.g. from ..somepackage import blahblah
, count the dots at the beginning (two), and then call this the import “level”. For each dot “level” you need a certain number of packages in __package__
separated by dots. E.g. if __package__
= folder.somepackage
then the above import will work because python runs len(__package__.split("."))
(= two) and then compares that to the number of dots. I’m not even kidding. If there there are not enough packages for the level it will throw the error: attempted relative import with no known parent package
.
Namespace vs “regular” packages
Before python 3.3 there were only “regular” packages: Some folder with an __init__.py
file in it. 3.3 added “namespace” packages. The documentation on these are sparse / very badly written and the pep is absolutely awful.
Namespace packages: Can be safely ignored as a concept if you don’t need to spread a package out over multiple folders (i.e. most use cases). See below if you’re really interested in the topic otherwise just skip this and be sure to use regular packages:
Regular packages: A folder with an __init__.py
file in it.
What a namespace package is:
container1/
└── spreadoutpackage/
└── lowercase.py
container2/
└── spreadoutpackage/
└── niceprint.py
Note that spreadoutpackage
has the same name in two different folders and that there are no __init__.py
files. Succinctly: if container1/
and container2/
are in the lookup path list, then these two calls from a python file:
from spreadoutpackage.lowercase import CaseLowerer
, and:from spreadoutpackage.niceprint import print_this
,
will work (whereas before python 3.3 it wouldn’t).
This means you can spread a single package out over many folders. It’s like a “virtual module”.
The relevance to imports is that you might have been unintentionally importing namespace packages if you have a python project folder and you didn’t create __init__.py
files (which can lead to confusing behavior).
References
5. The import system. (2023). Python Documentation. Retrieved October 17, 2023, from https://docs.python.org/3/reference/import.html
6. Modules. (2023). Python Documentation. Retrieved October 17, 2023, from https://docs.python.org/3/tutorial/modules.html
9. Top-level components. (2023). Python Documentation. Retrieved October 17, 2023, from https://docs.python.org/3.12/reference/toplevel_components.html
Cpython importlib/_bootstrap.py source code. (2023, August 29). Retrieved October 17, 2023, from https://github.com/python/cpython/blob/3.12/Lib/importlib/_bootstrap.py
Cpython importlib/init.py source code. (2023, May 3). Retrieved October 17, 2023, from https://github.com/python/cpython/blob/3.12/Lib/importlib/__init__.py
Glossary. (2023). Python Documentation. Retrieved October 17, 2023, from https://docs.python.org/3/glossary.html
main — Top-level code environment. (2023). Python Documentation. Retrieved October 17, 2023, from https://docs.python.org/3/library/__main__.html
Packaging namespace packages — Python Packaging User Guide. (2023, June 16). Python.org. Retrieved October 17, 2023, from https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
PEP 328 – imports: Multi-line and absolute/relative. (2003, December 21). Python.org. Retrieved October 17, 2023, from https://peps.python.org/pep-0328/
PEP 338 – Executing modules as scripts. (2004, October 16). Python.org. Retrieved October 17, 2023, from https://peps.python.org/pep-0338/
PEP 366 – Main module explicit relative imports. (2007, May 01). Python.org. Retrieved October 17, 2023, from https://peps.python.org/pep-0366/
PEP 420 – implicit namespace packages. (2012, April 19). Python.org. Retrieved October 17, 2023, from https://peps.python.org/pep-0420/
runpy — Locating and executing Python modules. (2023, August 7). Python Documentation. Retrieved October 17, 2023, from https://docs.python.org/3/library/runpy.html
sys — System-specific parameters and functions. (2023). Python Documentation. Retrieved October 17, 2023, from https://docs.python.org/3/library/sys.html
The initialization of the sys.path module search path. (2023). Python Documentation. Retrieved October 17, 2023, from https://docs.python.org/3/library/sys_path_init.html
What is the purpose of the -m switch? (2011, September 30). Stack Overflow. Retrieved October 17, 2023, from https://stackoverflow.com/questions/7610001/what-is-the-purpose-of-the-m-switch
Which, for whatever reason, is in a variable called
sys.modules
. You canimport sys
thenprint(sys.modules)
in a python file to see the cache. ↩︎Python stores this list of paths in the variable
sys.path
. This variable is initialized from a set of defaults (like C:\Python311\Lib\site-packages, etc), and the current folder where python is being executed. ↩︎Python executes files as they’re imported. ↩︎
Running a file directly like this in python sets it manually as the the “
__main__
” file. The “main” file has its__name__
variable set to__main__
by python interpretor (see point above also). The idea being to have an “entry point” of execution into a python program. From the docs: The file designated as__main__
“provides the local and global namespace for execution of the complete program” ↩︎Trivia: When running using the “-m switch” python will run the code / folder, etc with a program called “runpy” ↩︎
Need to have a
__main__.py
file in the folder—which will be run and considered the main file automatically (see footnote 4). ↩︎