ST008: ExportConsistencyCheck
Overview
| Property | Value |
|---|---|
| ID | ST008 |
| Name | ExportConsistencyCheck |
| Group | structure |
| Severity | WARNING |
Description
Checks that all names listed in __all__ are actually defined or imported in the module.
This is similar to R CMD check’s NAMESPACE validation, which verifies that all exported names in R packages actually exist. In Python, __all__ controls what from package import * exposes and documents the public API.
What it checks
The check:
- Finds all
__init__.pyfiles in the package - Parses each file using AST to extract:
- The
__all__list (if defined statically) - All defined names (functions, classes, variables, imports)
- The
- Compares
__all__entries against defined names - Reports any names in
__all__that aren’t defined
Why it matters
Python silently fails when __all__ contains non-existent names, leading to runtime ImportError or AttributeError when users try to import them:
# mypackage/__init__.py
__all__ = ["foo", "bar", "missing_func"] # missing_func doesn't exist!
def foo():
pass
def bar():
pass# User code
from mypackage import missing_func # ImportError!Common causes:
- Typos in
__all__:"funtion"instead of"function" - Deleted functions: Function was removed but
__all__wasn’t updated - Renamed functions: Function was renamed but
__all__still has old name - Failed imports: Re-export from submodule that doesn’t exist
How to fix
Remove undefined names
If a name was intentionally removed, update __all__:
# Before
__all__ = ["foo", "bar", "deleted_func"]
# After
__all__ = ["foo", "bar"]Define the missing name
If a name should exist, add it:
__all__ = ["foo", "bar", "new_func"]
def foo():
pass
def bar():
pass
def new_func(): # Add the missing function
passImport the missing name
If re-exporting from a submodule:
from .submodule import helper # Import the name
from .utils import format_output
__all__ = ["helper", "format_output"]Fix typos
# Before
__all__ = ["proces_data"] # Typo!
def process_data():
pass
# After
__all__ = ["process_data"] # Fixed
def process_data():
passEdge cases
Dynamic __all__
The check returns NOT_APPLICABLE when __all__ is dynamic:
# Cannot statically analyze
__all__ = get_exports()
__all__ = list(PUBLIC_NAMES)Star imports
When a file uses star imports, some names may come from the imported module:
from .submodule import *
__all__ = ["foo", "bar"] # bar might come from submodule
def foo():
passThe check handles this gracefully, understanding that some names may come from star imports.
TYPE_CHECKING blocks
Names imported only for type checking are not counted as defined:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from expensive import HeavyType # Not available at runtime
__all__ = ["HeavyType"] # This would fail!Configuration
Skip this check
[tool.pycmdcheck]
skip = ["ST008"]CLI
pycmdcheck --skip ST008