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 sysView on GitHub (pinned to d7d0d0a394)
Solutions
- Inspect the offending wheel's entry_points.txt (or *.dist-info METADATA) to find the malformed entry point.
- Fix the source declaration: ensure every script maps to 'name = package.module:object' (with the colon and callable).
- Rebuild the wheel and reinstall: pip wheel . --no-deps && pip install dist/<pkg>-*.whl.
- 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
- Lint [project.scripts] entries in CI to ensure 'name = module:callable' form.
- Validate wheels before publishing: check every entry_point has a colon and callable.
- Use a PEP 621-compliant pyproject.toml with a modern backend.
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
- Invalid script entry point name {entry.name!r}: the script w
- Unexpected file in {wheel_path}: {record_path!r}. .data dire
- Unknown scheme key used in {wheel_path}: {scheme_key} (for f
- For req: {req_description}. {e.args[0]}
- The wheel {wheel_path!r} has a file {target_path!r} trying t
AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04).
Data as JSON: /data/errors/5ed05a3a6bdfacd7.json.
Report an issue: GitHub.