pypa/pip · error · MissingCallableSuffix

Invalid script entry point: {entry_point} - A callable suffi

Error message

Invalid script entry point: {entry_point} - A callable suffix is required. See https://packaging.python.org/specifications/entry-points/#use-for-scripts for more information.

What it means

Raised as MissingCallableSuffix (an InstallationError subclass) when a console_scripts/gui_scripts entry point in a wheel is missing the 'module:callable' portion. At wheel.py:402-403, _raise_for_invalid_entrypoint parses the spec with distlib's get_export_entry; if the parsed ExportEntry has suffix is None (e.g. the spec is 'foo' with no '= module:func'), pip aborts the install rather than generating a broken script.

Source

Thrown at src/pip/_internal/operations/install/wheel.py:403


class MissingCallableSuffix(InstallationError):
    def __init__(self, entry_point: str) -> None:
        super().__init__(
            f"Invalid script entry point: {entry_point} - A callable "
            "suffix is required. See https://packaging.python.org/"
            "specifications/entry-points/#use-for-scripts for more "
            "information."
        )


def _raise_for_invalid_entrypoint(specification: str, scripts_dir: str) -> None:
    entry = get_export_entry(specification)
    if entry is None:
        return

    if entry.suffix is None:
        raise MissingCallableSuffix(str(entry))

    # distlib joins the entry point name onto the scripts directory, so a name
    # with path separators or ``..`` components can resolve elsewhere. The script
    # must resolve to a path strictly inside the scripts directory.
    dest = os.path.join(scripts_dir, entry.name)
    resolves_to_scripts_dir = os.path.abspath(dest) == os.path.abspath(scripts_dir)
    if resolves_to_scripts_dir or not is_within_directory(scripts_dir, dest):
        raise InstallationError(
            f"Invalid script entry point name {entry.name!r}: the script "
            f"would be installed outside the scripts directory ({scripts_dir})."
        )


class PipScriptMaker(ScriptMaker):
    # Override distlib's default script template with one that
    # doesn't import `re` module, allowing scripts to load faster.
    script_template = textwrap.dedent("""\
        import sys

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Inspect the offending wheel's entry_points.txt (or *.dist-info METADATA) to find the malformed entry point.
  2. Fix the source declaration: ensure every script maps to 'name = package.module:object' (with the colon and callable).
  3. Rebuild the wheel and reinstall: pip wheel . --no-deps && pip install dist/<pkg>-*.whl.
  4. If installing someone else's broken wheel, pin to a known-good version or open an upstream issue.

Example fix

# before (pyproject.toml)
[project.scripts]
mycli = "mypkg.cli"

# after
[project.scripts]
mycli = "mypkg.cli:main"
Defensive patterns

Strategy: validation

Validate before calling

from pip._vendor.distlib.util import get_export_entry
import zipfile, configparser, io

def validate_wheel_entrypoints(whl):
    with zipfile.ZipFile(whl) as z:
        for name in z.namelist():
            if name.endswith("entry_points.txt"):
                cp = configparser.ConfigParser()
                cp.read_string(z.read(name).decode())
                for section in ("console_scripts", "gui_scripts"):
                    for ep_name, spec in cp.items(section):
                        entry = get_export_entry(f"{ep_name} = {spec}")
                        if entry is None or entry.suffix is None:
                            raise ValueError(f"entry point '{ep_name}' missing callable: {spec}")

Type guard

def has_callable_suffix(spec: str) -> bool:
    from pip._vendor.distlib.util import get_export_entry
    entry = get_export_entry(spec)
    return entry is not None and entry.suffix is not None

Prevention

When it happens

Trigger: A wheel declares an entry point like 'mypkg' (bare name) or 'mypkg =' (no callable) in [project.scripts] / [console_scripts]. The entry resolves via get_export_entry to a name with no suffix, tripping the check before script generation.

Common situations: A broken setup.py/setup.cfg/pyproject.toml where [console_scripts] omits the callable, or a hand-edited entry_points.txt. Seen when a maintainer writes 'mycli' instead of 'mycli = mypkg.cli:main', or after a botched sdist-to-wheel build.

Related errors


AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04). Data as JSON: /data/errors/5ed05a3a6bdfacd7.json. Report an issue: GitHub.