private-import-path (SMP310)

SMP310No fixImports and dependenciesAll rules

Flags private imports that reach beyond one direct relative module, and imports that bind a private module as an object. In tests, imports from private modules must match the test’s path and name.

Why is this bad?

Importing a module such as package._api exposes its implementation path to callers. Importing names directly lets a package keep its implementation private while choosing what to expose through __all__. A parent-relative import such as from .._api import API also crosses a package boundary.

Example

Flagged

package/consumer.py

1import package._api as api
2
3value = api.make()
Preferred

package/consumer.py

1from ._api import make
2
3value = make()

What it does

The rule checks imports in module, class and function bodies. It offers no fix for an import alone: choosing a public re-export or replacing uses of a module object may require changes elsewhere.

Active on

  • Private module-object imports, such as import package._api as api and from . import _api.

  • Absolute private imports, such as from package._api import API. Use from ._api import API within the same package, or import API from a public re-export elsewhere.

  • Parent-relative private imports, such as from .._api import API and from .. import _api.

  • Paths with two private names, such as from ._internal._helpers import API or from ._internal import _helper. A direct relative import may cross one private name, not two.

  • Private modules below a sibling, such as from ._api.helpers import API.

  • Unpaired test imports, such as tests/package/test_api.py importing from package._other. A test of a private module may import from the matching module only; other behavior should be reached through public interfaces.

Exceptions

  • One private name in a direct relative import: from ._api import API lets the package use a private sibling to build an API outside tests. from .public import _helper also stays within the same package level.

  • A paired test and private module: tests/package/test_api.py may import names from package._api, and a colocated package/test_api.py may import names from package._api or ._api. The package path and the stem after test_ must match. A relative import from the mirrored test tree resolves inside that tree, so it is not paired with the source module. Importing the private module as an object remains disallowed.

  • Dunder names: __future__ and __version__ do not count as private names.

  • Documented public underscore names: os._exit is a public API despite its underscore.

Other tools

Ruff’s import-private-name (PLC2701) reports private imports across packages, but exempts relative imports and absolute imports within the same top-level package. This rule checks those paths and module-object imports too.