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:

  1. Finds all __init__.py files in the package
  2. Parses each file using AST to extract:
    • The __all__ list (if defined statically)
    • All defined names (functions, classes, variables, imports)
  3. Compares __all__ entries against defined names
  4. 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
    pass

Import 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():
    pass

Edge 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():
    pass

The 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