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](https://packaging.python.org/en/latest/guides/creating-command-line-tools/) and [installing standalone command-line tools](https://packaging.python.org/en/latest/guides/installing-stand-alone-command-line-tools/), plus the [entry-points specification](https://packaging.python.org/en/latest/specifications/entry-points/).
## Project layout
```text
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.
```python
# 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:
```toml
[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
```bash
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`:
```bash
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:
```bash
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.
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.
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.
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:
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
Identify the current command name, arguments, exit codes, configuration, and dependencies.
Move behavior into an importable package and a small main function.
Add pyproject.toml metadata and a console entry point.
Add tests for help, invalid input, normal output, and nonzero failures.
Install the wheel in a clean environment on every supported platform.
Document pipx install, upgrade, and uninstall commands.
Remove the old symlink only after command -v code-collector resolves to the packaged launcher and acceptance tests pass.
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.