How I Work: pipx

Published: Monday, 11 November 2020 by Michael Twomey

The Problem

You want to install command line Python tools and make them available. You want to type ipython or vd and have it run a version you installed.

You could pip install into the system Python but that leads to all sorts of fun (what happens when you upgrade your Python or when two CLI tools have conflicting dependencies, or won’t even work with your system Python?).

You could pip install --local and shunt it into your ~/.local/bin. This mostly works but has some of the same problems as above. At least it’s not messing with your system.

You could use Homebrew but I’ve found two problems with this: the first is you are beholden to the Homebrew upgrade schedule and, this affects me anyway, the second is every time you upgrade the Homebrew Python it seems to mess up all your brew installed python apps.

If you’re on Linux you could use whatever comes with your package manager but 99% of the time it’s a very old version of the tool.

I also experimented with creating a ~/.virtualenv/tool virtualenv per tool and symlinking into my ~/.local/bin, this mostly works. Can be a pain to manage though.

A Solution: pipx

pipx install sometool creates a virtualenv, pip installs the tool and makes all the bin/* commands available in ~/.local/bin. As a bonus you can easily upgrade versions and have different pythons for each tool. As a further bonus each tool can have wildly incompatible library dependencies without any problems as each lives in a separate virtual environment.

Installation

I usually opt for the non-homebrew installation instructions:

❯ python3 -m pip install --user pipx
❯ python3 -m pipx ensurepath

# Optional: shell completions
❯ pipx completions

This plays well with asdf to have a completely self contained setup.

Usage

Usage is pretty straight forward: where you would use pip use pipx.

❯ pipx install jupyterlab
  installed package jupyterlab 2.2.9, Python 3.9.0
  These apps are now globally available
    - jlpm
    - jupyter-lab
    - jupyter-labextension
    - jupyter-labhub
done! ✨ 🌟 ✨

❯ which jupyter-lab
/Users/mick/.local/bin/jupyter-lab

Upgrading a Package

You can upgrade a package using the upgrade command:

❯ pipx upgrade ipython
ipython is already at latest version 7.19.0 (location: /Users/mick/.local/pipx/venvs/ipython)

More interesting is the upgrade-all which upgrades everything:

❯ pipx upgrade-all
Versions did not change after running 'pip upgrade' for each package 😴

Injecting More Packages Into a Tool

This is really handy for somehing like ipython which usually wants a bunch of other libraries like pandas or numpy.

You can use the inject command to do this:

❯ pipx inject ipython pytz iso8601 tzdata
  injected package pytz into venv ipython
done! ✨ 🌟 ✨
  injected package iso8601 into venv ipython
done! ✨ 🌟 ✨
  injected package tzdata into venv ipython
done! ✨ 🌟 ✨

You can see what’s injected with the list --include-injected command:

❯ pipx list --include-injected
venvs are in /Users/mick/.local/pipx/venvs
apps are exposed on your $PATH at /Users/mick/.local/bin
...
   package ipython 7.19.0, Python 3.9.0
    - iptest
    - iptest3
    - ipython
    - ipython3
    Injected Packages:
      - iso8601 0.1.13
      - pytz 2020.1
      - tzdata 2020.2
...

Daily Actions

Every day I usually run this:

❯ pipx upgrade-all

This keeps my pipx managed CLI tools fresh.

A Note on Fiddly C Dependencies

Some packages don’t have pre-made wheels for your particular platform and Python combination, so you might need to help them and point to libraries for C extensions.

This is typically done by setting environment variables to point at libraries for the C builds.

For example: on my setup pgcli couldn’t easily build psycopg2 which depended on OpenSSL. Since I’m using Homebrew I need to give a hand locating dependencies:

# sh/bash/zsh
PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig" \
CPPFLAGS="-I/usr/local/opt/openssl@1.1/include" \
LDFLAGS="-L/usr/local/opt/openssl@1.1/lib" \
pipx install pgcli

# fish
set -lx PKG_CONFIG_PATH "/usr/local/opt/openssl@1.1/lib/pkgconfig"
set -lx CPPFLAGS "-I/usr/local/opt/openssl@1.1/include"
set -lx LDFLAGS "-L/usr/local/opt/openssl@1.1/lib"
pipx install pgcli