Skip to content
← All Python articles

Python explainer

Packages Without __init__.py

About 4 min to read. Allow extra time to work through examples.

See what Python does with a folder that has no __init__.py, which tools that breaks, and how several distributions can share one package name.

This article uses Python. You can make it your practice mode to browse related content across the site.

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 in setup.py, finds only regular packages. It leaves reports/ out of the wheel, so your tests pass in the project folder and the installed package has no reports. The [tool.setuptools.packages.find] table in pyproject.toml includes namespace packages by default.
  • pytest's default import mode imports a test file in a folder without __init__.py under its file name alone. Two files named test_helpers.py, in tests/unit/ and tests/integration/, then collide.
  • A namespace package has no __init__.py to hold re-exports, and its __file__ is None.

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

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.

step 8 of 41

Modules, Packaging and Tooling

Up next Fix a Circular Import