PEP 420 made __init__.py optional. A directory on sys.path with no
__init__.py is still importable — as a namespace package, assembled from
every matching directory across every path entry.
That is a real feature with a narrow, genuine use. It is also, in most repositories, a mistake nobody has noticed yet.
The accident
Somebody adds tests/helpers/ and forgets the __init__.py. Imports keep
working, tests keep passing, and the repository now contains a namespace
package that nobody designed. It stays invisible until one of these happens:
-
Packaging silently drops files.
setuptools.find_packages()looks for__init__.py; it does not find your directory, and the wheel ships without it.find_namespace_packages()finds it — along withdocs/,tests/and anything else that happens to be a directory. -
Test collection gets confused. Two
tests/helpers/util.pyfiles under different directories, neither with__init__.py, are both moduleutil. pytest’s default import mode gives you an “import file mismatch” error that reads like a bug in pytest. - mypy follows a different tree than you expect. A namespace portion can be assembled from directories in more than one place, and “which one is it checking” stops being obvious.
Ruff has a rule for exactly this: INP001, implicit namespace package,
which flags a file that is part of an implicit namespace package. If the
omission was deliberate you silence it deliberately; if it was an accident
you have found it.
💡Your repository has src/mypkg/ with an __init__.py and tests/helpers/ without one. Is tests.helpers a namespace package, and does it matter?
click to reveal
It is one only if tests/ is itself reachable as a package or namespace
portion from a sys.path entry — and in a typical layout, running pytest
from the project root puts the root on the path, so yes: tests and
tests.helpers both resolve as namespace packages.
It matters in one very specific and very annoying way. Without
__init__.py, module names are not qualified by their directory, so
tests/unit/util.py and tests/integration/util.py are both plain util.
Whichever is imported first wins, the second produces an import-file-mismatch
error, and the fix looks like magic when you find it.
Adding __init__.py to your test directories costs nothing and removes the
whole class of problem. This is the common case where the answer is “yes,
just add the file”.
The legitimate use
One namespace, split across several independently-released distributions. This is the pattern behind things like a plugin ecosystem, or a company namespace where each team ships its own package:
# distribution A # distribution B
acme/ acme/
storage/ auth/
__init__.py __init__.py
s3.py oidc.py
pip install acme-storage acme-auth and both acme.storage and acme.auth
import. Neither distribution owns acme; it exists only as the union of
portions found along sys.path. Nothing needs coordinating at release time,
which is the entire point.
The rule that breaks it
Every contributing distribution must omit __init__.py from the shared
namespace directory. If one of them ships a regular acme/__init__.py, that
directory becomes a regular package, the search stops there, and every other
distribution’s portion becomes unimportable. The failure is a
ModuleNotFoundError for a package the user definitely installed, and it is
caused by a file in a different distribution.
There is a legacy alternative you will still meet — pkgutil-style
__init__.py files containing __path__ = __import__('pkgutil').extend_path(...),
and pkg_resources-style declarations. Do not start a new namespace that
way, and do not mix styles inside one namespace.
The performance argument for regular packages
There is a genuine cost, and the import reference states it plainly: creation
and loading of a regular package can take place immediately once it is
located along the path — the finder sees __init__.py and stops. A namespace
package cannot stop. It must continue scanning every remaining sys.path
entry, because a later entry might contribute another portion.
On a laptop with a short path this is invisible. On a container image with a
long sys.path, or a network filesystem, or a serverless cold start where
every stat() is billed, “scan the whole path for every namespace import” is
a measurable and entirely avoidable cost.
💡A colleague proposes deleting every __init__.py in the repository, on the grounds that PEP 420 made them optional and fewer files is simpler. Give the two strongest objections.
click to reveal
You lose the ability to state that a directory is a package. An
__init__.py is the only in-band way to say “this directory is a unit with a
public surface”. Without it, find_packages() cannot find it, tooling
cannot distinguish a package from a directory of loose files, and there is
nowhere to put __all__ or a curated set of re-exports.
You make every import of it slower and less predictable. Each namespace
import scans the entire remaining sys.path instead of stopping at the first
hit, and the resulting package’s contents depend on what else happens to be
installed — which is a feature when you designed for it and a source of
irreproducible bugs when you did not.
The rule that holds up: use a namespace package when two or more
separately released distributions must contribute to one import name. In
every other case, write the __init__.py.
The decision, in one line
Omit __init__.py only when more than one distribution must contribute
to the same import name, and then omit it everywhere in that namespace.
Otherwise write it, even if it is empty — an empty file is cheaper than the
hour somebody will spend on the import-file-mismatch error.