Table of contents
What is a Package?
A Python package is a way of organizing related modules into a directory hierarchy.
Essentially, it's a directory that contains multiple Python modules along with a special __init__.py
file which allows the directory to be recognized as a package by Python.
Packages provide a method of grouping related Python code into a namespace.
The namespace provided by a package helps prevent naming clashes between different areas of functionality.
When you import a package, Python searches through the directories on sys.path looking for the package subdirectory.
Base example
Let's create the following project structure:
š my_project/
āāā š my_math/
ā āāā š __init__.py
ā āāā š add.py
ā āāā š sub.py
āāā š main.py
In the my_math package, we have two modules: add.py and sub.py.
The __init__.py file is required to make Python treat the my_math directory as a package.
def add(a, b):
return a + b
def sub(a, b):
return a - b
from .add import add
from .sub import sub
Note: If you leave __init__.py empty, Python will still recognize the directory as a package;
this allows you to import modules from this package.
from my_math import add, sub
print(add(1, 2)) # 3
print(sub(3, 1)) # 2
or
import my_math
print(my_math.add(1, 2)) # 3
print(my_math.sub(3, 1)) # 2
Now instead of having all the code in one file, we have separated the code into different modules.
Moreover, we have grouped the modules into a package called my_math.
Instead of importing the functions directly from the modules, we import them from the package.
Packages help to keep Python code organized, reusable, and clear to both the original developer and others who may use their code. They are a fundamental part of structuring Python applications.
Subpackages
Packages can be nested ā a package can contain other packages, forming a hierarchy. This is very common in real-world projects where you want to further group related modules.
Let's extend the previous example with a subpackage:
š my_project/
āāā š my_math/
ā āāā š __init__.py
ā āāā š add.py
ā āāā š sub.py
ā āāā š advanced/
ā āāā š __init__.py
ā āāā š power.py
āāā š main.py
def power(base, exp):
return base ** exp
from .power import power
from my_math.advanced import power
print(power(2, 10)) # 1024
Each subdirectory needs its own __init__.py to be treated as a package.
Relative vs Absolute Imports
You may have noticed the dot (.) prefix in imports like from .add import add inside __init__.py.
This is a relative import ā the dot means "look in the current package".
# Relative import ā looks relative to the current package
from .add import add
# Absolute import ā looks from the project root / sys.path
from my_math.add import add
Both styles work, but relative imports are common inside packages to avoid hardcoding the package name.
Absolute imports are generally preferred in application-level code (e.g. main.py) as they are more explicit and easier to follow.
Note: Python searches for packages by scanning directories listed in sys.path.
When you run a script, Python automatically adds its directory to sys.path, which is why
from my_math import ... works when running main.py from the project root.
Controlling Exports with __all__
You can control what gets exported when someone does from my_math import * by defining __all__ in your __init__.py:
__all__ = ["add", "sub"]
from .add import add
from .sub import sub
Without __all__, import * pulls in everything that doesn't start with an underscore.
Defining it explicitly makes your package's public API clear and intentional.
Tip: Even if you don't use import * yourself, defining __all__ is good practice ā it signals
to other developers (and tools like linters) what is considered part of the public interface.
