Package and Install a Python Command-Line Application

A shebang, executable bit, and symlink can expose one local script, but they do not describe dependencies, create an isolated environment, support Windows, or provide a reliable upgrade and uninstall path. A maintained CLI should be packaged as a Python application and installed in an isolated environment.

This guide was verified on 2026-08-01 against the PyPA guides for creating command-line tools and installing standalone command-line tools, plus the entry-points specification.

Project layout

1
2
3
4
5
6
7
8
code-collector/
├── pyproject.toml
├── src/
│   └── code_collector/
│       ├── __init__.py
│       └── cli.py
└── tests/
    └── test_cli.py

Keep command behavior in a callable function. Return an exit code instead of calling sys.exit deep inside business logic.

1
2
3
4
5
6
7
8
9
# src/code_collector/cli.py
import argparse

def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("path")
    args = parser.parse_args()
    print(args.path)
    return 0

Declare the command

One standards-based pyproject.toml example using Hatchling is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "code-collector"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = []

[project.scripts]
code-collector = "code_collector.cli:main"

The [project.scripts] entry creates a platform-appropriate launcher. The same package can work on macOS, Linux, and Windows without asking users to create symlinks manually. Select and pin a maintained build backend according to repository policy; Hatchling is an example, not a universal requirement.

Develop and test safely

1
2
3
4
5
6
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --editable .
code-collector --help
python -m pytest
python -m build

An editable install is useful during development because source changes are visible without reinstalling. Build both wheel and source distribution before release, inspect their contents, and test the wheel in a fresh environment. Avoid importing or executing project code from build configuration.

Install as an application

For a local or published CLI, pipx gives each application its own virtual environment while exposing its commands on PATH:

1
2
3
4
pipx install .
code-collector --help
pipx upgrade code-collector
pipx uninstall code-collector

Users can install from an approved package index or an explicitly trusted source. Do not encourage installation from an unreviewed URL, and never embed index credentials in documentation or repository files.

Alternatives and tradeoffs

  • A shell alias is adequate for a personal one-line convenience command, but is not a distributable application.
  • A repository-local script can be run as python path/to/script.py; this is simple when only contributors use it.
  • A zipapp can distribute some pure-Python applications as one archive, but dependency and native-extension constraints require care.
  • Tools such as Typer or Click provide richer CLI ergonomics; argparse avoids an external runtime dependency.
  • Native launchers or standalone bundlers can remove the requirement for a user-managed Python, at the cost of larger artifacts and platform-specific builds.

Migration checklist

  1. Identify the current command name, arguments, exit codes, configuration, and dependencies.
  2. Move behavior into an importable package and a small main function.
  3. Add pyproject.toml metadata and a console entry point.
  4. Add tests for help, invalid input, normal output, and nonzero failures.
  5. Install the wheel in a clean environment on every supported platform.
  6. Document pipx install, upgrade, and uninstall commands.
  7. Remove the old symlink only after command -v code-collector resolves to the packaged launcher and acceptance tests pass.

Verification gate

Record the verification date and run:

1
2
3
4
python -m build
python -m pip install --force-reinstall dist/*.whl
code-collector --help
python -m pytest

Also verify signal handling, filesystem permissions, encoding, and exit status for the environments the CLI actually supports.

Release and maintenance

Build releases from a tagged, clean commit and publish immutable versioned artifacts. When publishing to PyPI from CI, use trusted publishing where available instead of storing a long-lived token. Generate provenance evidence when the distribution context requires it.

Treat stdout and stderr as interfaces. Keep machine-readable output stable or versioned, send diagnostics to stderr, and document exit codes. Avoid update checks or telemetry that delay startup or violate user expectations. When dropping a Python version, moving configuration, or renaming an option, publish a migration note and test the old failure path so users receive an actionable message. Assign ownership for dependency updates, supported platforms, vulnerability response, and retirement of obsolete releases.