Project Setup

Installing and Distributing CLIs with pipx

Use pipx to install Python CLIs into isolated environments, expose their commands globally, upgrade or pin them, and run one-off tools with pipx run.

Updated

When you install a Python CLI with plain pip install, its dependencies land in whatever environment you happened to be in — often your system Python — where they collide with the next tool's dependencies. pipx fixes this: it gives every CLI its own private virtual environment and links just the command onto your PATH. You get global commands with zero dependency conflicts. This guide covers the isolation model, the day-to-day commands, installing from a wheel/git/PyPI, and when to reach for pipx versus a plain venv.

TL;DR

  • One venv per tool, one command on your PATH. pipx install black puts Black in its own environment and exposes only the black command.
  • pipx run for one-offs. pipx run cowsay Hello fetches, runs in a cache, and leaves nothing installed.
  • pipx inject to add plugins into a tool's private environment without touching your own.
  • Pin and upgrade explicitly. pipx install "httpie==3.2.2" pins; pipx upgrade httpie bumps; pipx upgrade-all maintains everything.
  • Run pipx ensurepath once so the linked commands are actually found.

Each pipx-installed CLI lives in its own virtual environment while its command is symlinked onto the user's PATH, keeping tools isolated from each other and from system Python.

What pipx is and the isolation model

pipx is a tool for installing and running Python applications — programs you invoke by name, not libraries you import. Its whole design is one idea: isolate the environment, expose the command. For each package you install, pipx creates a dedicated virtual environment under ~/.local/pipx/venvs/<name>/, installs the package and its dependencies there, then creates a symlink (or a small launcher on Windows) in ~/.local/bin/ pointing at each console entry point the package declares.

The payoff is that two tools with conflicting requirements — say one needing rich<13 and another needing rich>=14 — coexist happily because neither can see the other's dependencies. It is the same principle as the isolated venvs in virtual environments and isolation best practices, automated for the specific case of installing command-line applications. Those console entry points are exactly the ones you declare in [project.scripts], covered in the packaging overview.

Install pipx itself (it is deliberately kept outside the environments it manages):

$ python -m pip install --user pipx
$ python -m pipx ensurepath      # add ~/.local/bin to PATH; restart your shell after

On macOS brew install pipx and on recent Debian/Ubuntu apt install pipx also work.

PATH setup with pipx ensurepath

The single most common "pipx installed it but the command isn't found" problem is that ~/.local/bin is not on your PATH. pipx ensurepath edits your shell profile to add it and tells you to restart the shell:

$ pipx ensurepath
Success! Added /home/you/.local/bin to the PATH environment variable.
You will need to open a new terminal or re-source your shell configuration...

Run it once per machine. In a Dockerfile or CI job where you cannot "restart the shell," set the variable directly instead:

ENV PATH="/root/.local/bin:${PATH}"

Install, list, upgrade, uninstall

The core lifecycle is four commands. Everything else is a variation on these.

$ pipx install httpie            # create a venv, install, link the `http` command
$ pipx list                      # show every installed app, its version, and its commands
$ pipx upgrade httpie            # reinstall the latest compatible version in its venv
$ pipx uninstall httpie          # remove the venv and the linked commands

pipx list is worth knowing well; it prints the Python version each venv uses and the exact commands exposed, which is how you discover that installing httpie gave you the http and https commands rather than an httpie command:

$ pipx list
venvs are in /home/you/.local/pipx/venvs
   package httpie 3.2.2, installed using Python 3.12.3
    - http
    - https

To keep everything current in one shot, pipx upgrade-all. To rebuild every environment against a new Python after an interpreter upgrade, pipx reinstall-all --python 3.13.

Pinning versions and choosing the interpreter

pipx install accepts any pip requirement specifier, so pinning is just standard version syntax. Pin when you need reproducibility — CI, or a tool whose latest release broke you:

$ pipx install "httpie==3.2.2"          # exact pin
$ pipx install "ruff>=0.6,<0.7"         # compatible range

Pick the interpreter a tool runs on with --python. This is how you keep a legacy tool on 3.11 while your default is 3.13, or test a tool across interpreters:

$ pipx install --python 3.11 some-legacy-cli
$ pipx install --python /usr/bin/python3.13 my-cli

Because each install is a full environment, the pin sticks: pipx upgrade will not move a tool off a version you pinned unless you reinstall without the constraint.

Installing from a wheel, a git URL, or PyPI

pipx installs from anywhere pip can, which makes it the natural way to hand someone a tool at any stage of its life.

# From PyPI (the published, public case)
$ pipx install greet-cli

# From a locally built wheel — great for sharing a pre-release
$ pipx install ./dist/greet_cli-0.1.0-py3-none-any.whl

# From a git repository, no release required (installs from a branch or tag)
$ pipx install "git+https://github.com/ada/greet-cli.git@main"

# From a subdirectory of a monorepo
$ pipx install "git+https://github.com/ada/tools.git#subdirectory=greet-cli"

The wheel case pairs directly with building wheels and sdists for Python CLIs: build the wheel, pipx install ./dist/*.whl, and you have verified the entire packaging chain end to end without touching a package index. The git case is the fastest way for a colleague to try your main branch before you have published anything to PyPI.

pipx run: tools you use once

Some tools you need exactly once — a project scaffolder, a one-off formatter, a diagnostic. Installing them permanently is clutter. pipx run fetches the package into a temporary cache, runs it, and installs nothing into your set of managed apps:

$ pipx run cowsay -t "shipped it"
$ pipx run --spec "cookiecutter" cookiecutter gh:audreyfeldroy/cookiecutter-pypackage

Use --spec when the command name differs from the package name, or to pin the one-off run (pipx run --spec "black==24.8.0" black .). The cache is reused for a while, so a repeated pipx run of the same tool is fast on the second call. This is the mechanism behind the "just run it" instructions in CLI project scaffolding with Cookiecutter.

pipx inject: adding plugins to a tool

Some CLIs load plugins that must live in the same environment as the tool — think a mkdocs theme or a pytest plugin you want globally. pipx inject installs extra packages into an existing app's private venv without polluting your own environment:

$ pipx install mkdocs
$ pipx inject mkdocs mkdocs-material          # add the theme into mkdocs' venv
$ pipx inject mkdocs mkdocs-material --include-apps   # also link any new commands

Without --include-apps, injected packages are importable by the tool but their own commands are not linked onto your PATH — which is usually what you want for a pure plugin.

When pipx, and when a plain venv

pipx is the right tool when the thing you are installing is an application you run by name and want available everywhere. Reach for a plain project virtual environment instead when:

  • You are developing the CLI, not just running it — you want an editable install (pip install -e . or uv pip install -e .) inside a project venv so code changes take effect immediately.
  • The tool is a library other code imports; pipx is for commands, not import targets.
  • You need the tool's dependencies available to your project's code, not sequestered in a private environment.

There is also a fast-moving alternative in the same niche: uv tool install does what pipx install does using uv's resolver and shared cache. The trade-offs are worth understanding before you standardize a team on one; see uv tool install vs pipx for CLIs.

Production notes

  • Document pipx as the install path in your README. For a published CLI, pipx install your-cli is the recommendation that spares users the "it broke my system Python" class of bug. Show pipx run your-cli too for the try-before-you-commit case.
  • CI has no interactive shell. Prefer setting PATH explicitly over relying on ensurepath, and pass --python to pin the interpreter so builds are reproducible.
  • pipx runpip <app> ... reaches into a tool's venv to inspect or debug it (e.g. pipx runpip httpie freeze) without breaking its isolation.
  • Reinstall after a Python upgrade. A venv built against a Python that gets removed will break; pipx reinstall-all rebuilds them against your current interpreter. On distros that upgrade system Python underneath you, this is the fix for "all my pipx tools stopped working."
  • Environment location is configurable. PIPX_HOME and PIPX_BIN_DIR relocate the venvs and the linked commands — useful for shared or read-only-home CI images.