You add a folder reports/ with a summary.py in it, and you forget the
__init__.py. The import still works:
import reports.summary
print(reports)
# <module 'reports' (namespace) from ['/home/you/code/app/reports']>
Python imported reports as a namespace package, a folder without
__init__.py that Python still imports as a package. A folder with an
__init__.py is a regular package.
A forgotten __init__.py breaks other tools
The import works, so you may not notice the missing file until another tool treats the folder differently:
- setuptools'
find_packages(), often used insetup.py, finds only regular packages. It leavesreports/out of the wheel, so your tests pass in the project folder and the installed package has noreports. The[tool.setuptools.packages.find]table inpyproject.tomlincludes namespace packages by default. - pytest's default import mode imports a test file in a folder without
__init__.pyunder its file name alone. Two files namedtest_helpers.py, intests/unit/andtests/integration/, then collide. - A namespace package has no
__init__.pyto hold re-exports, and its__file__isNone.
If you meant a regular package, add the empty __init__.py. For the pytest
collision, you can also run pytest with --import-mode=importlib. In that
mode, test files cannot import each other as plain modules, so check how your
tests import shared helpers.
Share one name across distributions
The real use for a namespace package is one import name shipped by several
distributions. Say your company publishes acme-billing and acme-auth
separately, and both import under acme:
dist_a/ dist_b/
acme/ acme/
billing/ auth/
__init__.py __init__.py
Neither acme folder has an __init__.py. Their children are regular
packages. When both folders are on the search path, Python combines them into
one acme package. Each folder is a portion of it.
Try it without installing anything. Create the folders above, with empty
__init__.py files where shown, and run this script from their parent folder:
import sys
from pathlib import Path
sys.path[:0] = [str(Path("dist_a").resolve()), str(Path("dist_b").resolve())]
import acme
import acme.auth
import acme.billing
print(list(acme.__path__)) # both acme folders
Editing sys.path is only for the demo. In a real project, pip installs both
distributions into site-packages, often into one shared acme folder. That
works too, because neither distribution ships acme/__init__.py.
A shared acme/__init__.py hides the other portions
Add an empty dist_a/acme/__init__.py and run the demo again in a fresh
process:
ModuleNotFoundError: No module named 'acme.auth'
A regular package wins over namespace portions, wherever it sits on the
search path. So acme is now the regular package in dist_a, its
__path__ holds only that folder, and Python never looks in dist_b. You
see this when the portions live in different folders, as with editable
installs. Every distribution that shares the name must leave out
acme/__init__.py.
Agree on which distribution owns each subfolder, too. Two distributions that ship the same file overwrite each other.
Check your understanding Your tests pass in the project folder. Does that prove the wheel contains every subpackage? Show answer Hide answer
No. Running from the project folder puts it on the search path, so Python
finds folders that the wheel may leave out. Build the wheel and install it in
a new virtual environment, a folder with its own Python and site-packages that
python -m venv .venv creates. Run the tests from outside the project folder.
Then print acme.__path__ and each subpackage's __file__ to see which files
Python loaded.
Going further
Give each distribution build settings that pick up only its own subpackage.
With setuptools and a src/acme/billing/ layout:
[tool.setuptools.packages.find]
where = ["src"]
include = ["acme.billing", "acme.billing.*"]
namespaces = true
Older projects built namespaces with pkgutil.extend_path or
pkg_resources in a shared __init__.py. Do not mix those with native
namespace packages. Move every distribution to one style, and test the full
set together.
See PEP 420, the setuptools package discovery guide and pytest's import modes.