Gaining a better understanding of Python modules and packages

This post attempts to clear frequently occurring confusion around Python modules and packages.
You should read this post if you frequently grapple with the following errors:
- Attempted relative import in non-package
- No module named package.module
- You want to understand how relative imports work
Modules
We will create two Python modules. These modules are without a package.
A Python package doesn’t come into picture unless there is an __init__.py
defined.
Let’s create a directory called anydir
in your home directory.
╰─$ mkdir anydir
╰─$ cd anydir
Let’s create a Python module called hello.py
with following code:
def hello():
return 'hello'
Let’s create another Python module called say_hello.py
with the following code:
from hello import hellodef say():
return 'saying %s' % hello()
We can start a Python shell and invoke say
.
In [1]: from say_hello import sayIn [3]: print say()
saying hello
Very often developers think that since hello.py
and say_hello.py
are in the same directory so they could use relative import instead of an absolute import.
They change from from hello import hello
to from .hello import hello
.
Let’s try that. Make say_hello.py
look like:
from .hello import hellodef say():
return 'saying %s' % hello()
Restart Python shell and invoke say
again.
In [1]: from say_hello import say
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-1-c19beb0ca355> in <module>()
----> 1 from say_hello import say/Users/akshar/anydir/say_hello.py in <module>()
----> 1 from .hello import hello
2
3 def say():
4 return 'saying %s' % hello()ValueError: Attempted relative import in non-package
Shell would have raised a ValueError
with message Attempted relative import in non-package
.
Relative imports can only be used in a package. Since there is no package here, so it failed.
Packages
Let’s create a package called mathematics
inside anydir
.
╰─$ pwd
/Users/akshar/anydir╰─$ mkdir mathematics╰─$ touch mathematics/__init__.py
We created an __init__.py
inside mathematics
, so mathematics
becomes a package.
Let’s create a module called simple.py
in package mathematics
.
╰─$ vim mathematics/simple.py
Add the following content to this module.
def sum(a, b):
return a + b
Let’s create another module called advanced.py
in package mathematics
.
╰─$ vim mathematics/advanced.py
Add the following content to this module.
from simple import sumdef add_five_to_sum(a, b):
return 5 + sum(a, b)
Let’s start a Python shell and invoke add_five_to_sum
.
In [1]: from mathematics.advanced import add_five_to_sumIn [2]: add_five_to_sum(3, 4)
Out[2]: 12
Let’s remove mathematics/__init__.py
and try to invoke add_five_to_sum
.
╰─$ rm mathematics/__init__.py╰─$ ipythonIn [1]: from mathematics.advanced import add_five_to_sum
---------------------------------------------------------------------------
ImportError Traceback (most recent call last)
<ipython-input-1-452b1a8afbc5> in <module>()
----> 1 from mathematics.advanced import add_five_to_sumImportError: No module named mathematics.advanced
Let’s add mathematics/__init__.py
back. Things should work as earlier then.
╰─$ touch mathematics/__init__.py
Let’s use relative import in mathematics/advanced.py
. Modify the import statement to look like the following:
from .simple import sum
Let’s invoke add_five_to_sum
.
╰─$ ipythonIn [1]: from mathematics.advanced import add_five_to_sumIn [2]: add_five_to_sum(3, 4)
Out[2]: 12
Let’s try to use add_five_to_sum
from inside the package i.e from the directory mathematics
.
╰─$ cd mathematics╰─$ ipythonIn [1]: from advanced import add_five_to_sum
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-1-300480d69d38> in <module>()
----> 1 from advanced import add_five_to_sum/Users/akshar/anydir/mathematics/advanced.py in <module>()
----> 1 from .simple import sum
2
3 def add_five_to_sum(a, b):
4 return 5 + sum(a, b)ValueError: Attempted relative import in non-package
Now we are working in a module context and not in a package context. So relative imports of advanced.py
raised an error.
This suggests that if you have a package then you need to work from the directory containing the package. You cannot work in the package directory itself.
Let’s come back to our base directory.
╰─$ cd ..╰─$ pwd
/Users/akshar/anydir
We can create a subpackage inside package mathematics
. Let’s create a package complex
inside mathematics
.
╰─$ mkdir mathematics/complex╰─$ touch mathematics/complex/__init__.py
Since complex
is a package, so we created an __init__.py
in it.
Let’s create a module called all.py
in subpackage complex
.
╰─$ vim mathematics/complex/all.py
Let’s add the following content.
from mathematics.simple import sumdef add_ten_to_sum(a, b):
return 10 + sum(a, b)
Let’s invoke add_ten_to_sum
.
╰─$ ipythonIn [1]: from mathematics.complex.all import add_ten_to_sumIn [2]: add_ten_to_sum(10, 5)
Out[2]: 25
We can use relative imports in mathematics/complex/all.py
.
from ..simple import sumdef add_ten_to_sum(a, b):
return 10 + sum(a, b)
Restart shell and things should keep working as earlier.
All this setup is only going to work properly as long as you are working from anydir
. We can consider anydir
as our base directory.
If you try to work from directory mathematics
things will break. It’s important to give a proper thought to your base directory and define packages in it and do imports accordingly.
Feel free to ask any question on modules and packages and I would be glad to answer.