Distributing Applications & CLI Tools

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the Python Reference bibliography.

birdtally is a library with a command, birdtally. Users who only want the command should not have to manage a virtual environment or risk breaking the system Python (PEP 668, see Environments & Externally Managed Pythons). This page covers the ways to ship the command, run on 2026-10-22 with pipx 1.18.4, Poetry 2.5.1 and CPython 3.14.6 against the local index of this section. Distribution channels beyond Python (Homebrew, winget, native binaries) are compared on Packaging & Distribution for CLI tools.

Entry points make the command

[project.scripts]
birdtally = "birdtally.cli:main"

When the wheel is installed, the installer writes a small wrapper named birdtally into the environment’s bin/ (Scripts\ on Windows, as birdtally.exe) that imports birdtally.cli and calls main(), using that environment’s Python. main() should return an exit code (the wrapper passes it to sys.exit). Nothing in the project needs a __main__ shebang file; gui-scripts do the same for windowed programs on Windows.

pipx: an environment per application

pipx installs each application into its own virtual environment and puts only its commands on PATH:

python -m pip install pipx                         # or the OS package
pipx install --index-url http://127.0.0.1:8084/simple/ birdtally
birdtally examples/survey.csv --by site
creating virtual environment...
installing birdtally...
  installed package birdtally 0.1.2, installed using Python 3.14.6
  These apps are now available
    - birdtally
โ€ฆ
done! โœจ ๐ŸŒŸ โœจ
{
  "north-marsh": 6,
  "east-dunes": 3
}

(the warning that the scratch PIPX_BIN_DIR is not on PATH removed; pipx ensurepath fixes that on a real machine). Optional dependencies go into the same environment with pipx inject birdtally rich (or pipx install "birdtally[pretty]"), after which birdtally survey.csv --pretty drew its table:

โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”“
โ”ƒ Species  โ”ƒ Count โ”ƒ
โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”ฉ
โ”‚ lapwing  โ”‚ 7     โ”‚
โ”‚ redshank โ”‚ 2     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

pipx upgrade birdtally, pipx list and pipx uninstall birdtally manage it. uv tool install is the equivalent in uv. Compared with pip install birdtally into a shared environment, nothing else can break the tool’s dependencies and removing it leaves nothing behind. Recommend pipx install birdtally in the README for users who want the command.

Poetry: file scripts

[project.scripts] covers Python entry points. A script that is a file (a shell script, a pre-written launcher) goes in [tool.poetry.scripts] with type = "file", and poetry-core ships it in the wheel’s .data/scripts/:

[tool.poetry.scripts]
birdtally-help = { reference = "bin/birdtally-help", type = "file" }
       62  2016-01-01 00:00   birdtally-0.1.2.data/scripts/birdtally-help
       48  2016-01-01 00:00   birdtally-0.1.2.dist-info/entry_points.txt

After pip install, the environment’s bin/ held both birdtally and birdtally-help, and birdtally-help printed birdtally survey helper: run birdtally <csv>. (Poetry 2.5.0 fixed poetry install not installing file scripts for the project itself.)

Shipping an environment

  • poetry bundle venv /opt/birdtally (poetry-plugin-bundle) installs the project and its locked dependencies into a fresh virtual environment that can be copied to machines with the same Python and platform; shown on Exporting Requirements & Bundling.

  • A container image with the environment, built as on tox, nox, pre-commit & Docker.

  • Executable archives: the standard library’s zipapp, and the third-party shiv and pex, pack an application and its pure-Python dependencies into one file that runs with a matching Python.

  • Frozen applications that include the interpreter: PyInstaller, and per platform py2exe (Windows), py2app (macOS) and Pynsist (Windows installers, buildable on Linux).

  • Operating-system packages (deb, rpm, Homebrew, winget) package the application for one platform’s package manager.

The smallest unit: a single-file script (PEP 723)

A script that declares its own requirements needs no project, no build and no index:

# /// script
# requires-python = ">=3.10"
# dependencies = ["rich>=13"]
# ///
"""Count sightings per species in a survey CSV and print a table."""
import csv
import sys
from collections import Counter

from rich.console import Console
from rich.table import Table

counts = Counter()
with open(sys.argv[1], newline="") as f:
    for row in csv.DictReader(f):
        counts[row["species"]] += int(row["count"])

table = Table("Species", "Count")
for species, n in counts.most_common():
    table.add_row(species, str(n))
Console().print(table)
pipx run count_birds.py examples/survey.csv
Installed 4 packages in 8ms
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”“
โ”ƒ Species  โ”ƒ Count โ”ƒ
โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”ฉ
โ”‚ lapwing  โ”‚ 7     โ”‚
โ”‚ redshank โ”‚ 2     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

pipx created a cached environment with rich for the script (a deprecation warning printed by uv, which this pipx used for the installation, about the machine’s UV_NATIVE_TLS setting removed). uv run count_birds.py does the same; with pip, pip install --requirements-from-script count_birds.py (pip 26.0) installs the requirements into the current environment, see pip: Dependency Groups & Inline Script Metadata. Share the file itself: by URL, in a gist, or in a repository.