Packages and namespaces¶
A module is a single .py file. A package is a directory of
modules, treated as a single importable unit. Most non-trivial
Python projects are packages — including almost everything on PyPI.
This tutorial covers how packages work, the role of __init__.py,
and the difference between absolute and relative imports.
Time commitment: 15-20 minutes
Prerequisites:
- You've worked through Modules and imports
Learning objectives¶
By the end of this tutorial, you will be able to:
- Lay out a package directory and explain the role of
__init__.py - Import from sub-packages with both absolute and relative imports
- Recognise and use namespace packages
From module to package¶
A package is a directory containing an __init__.py file. The
directory name is the package name, and the package's modules are
the .py files inside it. Sub-directories with their own
__init__.py files become sub-packages.
Here's the layout we'll build in this notebook:
shapes/
__init__.py
square.py
polygons/
__init__.py
triangle.py
Building it on the page¶
We can do this in the browser because Pyodide gives us a virtual
filesystem — os.makedirs and open(...).write() work just as they
would on disk. The files we create live in the page's memory and
disappear when you reload.
import os, sys
# Make sure the current directory is on the import path.
if "" not in sys.path:
sys.path.insert(0, "")
os.makedirs("shapes/polygons", exist_ok=True)
# __init__.py marks the directory as a package. It can be empty.
open("shapes/__init__.py", "w").close()
open("shapes/polygons/__init__.py", "w").close()
# A simple module inside the package.
open("shapes/square.py", "w").write(
"def area(side):\n"
" return side * side\n"
)
# And one inside the sub-package.
open("shapes/polygons/triangle.py", "w").write(
"def area(base, height):\n"
" return 0.5 * base * height\n"
)
# We can now import from our package as if it were any third-party library.
import shapes.square
import shapes.polygons.triangle
print(shapes.square.area(5))
print(shapes.polygons.triangle.area(4, 3))
What __init__.py is for¶
At minimum, __init__.py marks the directory as a package. But it's
also code that runs when the package is imported, which makes it the
natural home for a few things:
- Re-exports. Make a sub-module's name available at the package
level:
from .square import area. Nowshapes.areaworks without the user knowingsquare.pyexists. - Public API definition. Set
__all__ = ["area", ...]to declare which names should be considered the package's public surface. - Package-level constants. A
__version__string is conventional.
Avoid putting anything heavy or slow in __init__.py — it runs every
time anyone imports anything from the package.
# Promote `area` to the package level by editing __init__.py.
open("shapes/__init__.py", "w").write(
"from .square import area as square_area\n"
"from .polygons.triangle import area as triangle_area\n"
"__version__ = \"0.1.0\"\n"
)
# Force a fresh import so __init__.py runs again.
import importlib, shapes
importlib.reload(shapes)
print(shapes.square_area(5))
print(shapes.triangle_area(4, 3))
print(shapes.__version__)
Absolute vs relative imports¶
Inside the package, modules can refer to each other in two ways.
Absolute imports name the package from the top:
# inside shapes/polygons/triangle.py
from shapes.square import area
Relative imports use dots to mean "current package" and "parent package":
# inside shapes/polygons/triangle.py
from ..square import area # one dot up to `shapes`, then into `square`
from . import other_module # same package as me
Style guides (including PEP 8) recommend absolute imports for clarity. Relative imports are useful when a package might be renamed or vendored, since they don't hard-code the top-level name. Pick one style per project and be consistent.
Namespace packages¶
Since Python 3.3, a directory can be importable as a package
without an __init__.py — a so-called namespace package. This
is mostly useful when you want a single logical package whose contents
live in multiple physical directories (often on different sys.path
entries), typically for plugin-style architectures.
For everyday work, prefer regular packages with an __init__.py —
they're explicit, support per-package initialisation, and avoid a few
subtle gotchas around sys.path ordering.
Recap and next steps¶
- A package is a directory with an
__init__.py(regular) or without (namespace). __init__.pyis the natural place for re-exports,__all__, and version constants.- Prefer absolute imports for clarity; reach for relative imports when the package's name isn't fixed.
Next: how to bring code from outside your project into it — Installing third-party packages.