Packaging a CLI with entry points

Click · Advanced · 10 min

Users should type mytool, not python path/to/script.py, and packaging is how that happens.

To make a CLI installable as a command, declare a console script entry point in pyproject.toml:

[project.scripts]
mytool = "mytool.cli:cli"

The value is module:object, pointing to your Click command or group. When the package is installed (pip install ., pip install -e . for development, pipx install or uv tool install for end users), the installer generates a small executable called mytool on the PATH that imports the module and calls cli(). This works on all platforms, including creating .exe launchers on Windows.

A typical layout:

src/mytool/__init__.py
src/mytool/cli.py        # defines the click group `cli`
src/mytool/__main__.py   # from .cli import cli; cli()

The __main__.py file adds python -m mytool support, handy when the script directory is not on PATH.

Tips:

  • Pass prog_name if the displayed name should differ from the executable name; otherwise Click uses the invoked name in usage lines.
  • @click.version_option() reads the installed distribution's version via importlib.metadata; give package_name= if it cannot be detected.
  • Keep imports at the top of cli.py light, because every invocation, even --help and shell completion, imports the module. Import heavy libraries inside the command functions or use lazy loading.
  • Avoid if __name__ == "__main__" scripts as the main entry; entry points are more robust.

Example

# pyproject.toml
[project]
name = "mytool"
version = "1.2.0"
dependencies = ["click>=8.5"]

[project.scripts]
mytool = "mytool.cli:cli"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

# Then:
#   pip install -e .        (development)
#   pipx install .          (end users, isolated)
#   mytool --version        -> mytool, version 1.2.0

Remember

  • [project.scripts] name = 'package.module:command' creates the executable
  • Add __main__.py for python -m support
  • Keep CLI module imports light: they run on every invocation

Watch out for

  • Importing heavy libraries at module level, making even --help slow.

Check yourself

  1. What does mytool = "mytool.cli:cli" under [project.scripts] mean?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: click.palletsprojects.com/en/stable/entry-points/