Skip to content
← All articles

Namespace packages: when to omit __init__.py

Most namespace packages in the wild are accidents — somebody forgot an __init__.py and nothing broke, so nobody noticed until packaging, test collection or mypy started behaving strangely.

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 with docs/, tests/ and anything else that happens to be a directory.
  • Test collection gets confused. Two tests/helpers/util.py files under different directories, neither with __init__.py, are both module util. 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.