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_nameif 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 viaimportlib.metadata; givepackage_name=if it cannot be detected.- Keep imports at the top of
cli.pylight, because every invocation, even--helpand 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.0Remember
- [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.