Making packages compatible

Developing for pipx

To let your users run

$ pipx install MY_PACKAGE

include scripts in your main table [1] in pyproject.toml or its legacy equivalents for setup.cfg and setup.py. pipx also exposes gui-scripts entry points, which are useful for GUI applications on Windows (they launch without opening a console window).

[project.scripts]
foo = "my_package.some_module:main_func"
bar = "other_module:some_func"

[project.gui-scripts]
baz = "my_package_gui:start_func"
[options.entry_points]
console_scripts =
    foo = my_package.some_module:main_func
    bar = other_module:some_func
gui_scripts =
    baz = my_package_gui:start_func
setup(
    # other arguments here...
    entry_points={
        "console_scripts": [
            "foo = my_package.some_module:main_func",
            "bar = other_module:some_func",
        ],
        "gui_scripts": [
            "baz = my_package_gui:start_func",
        ],
    },
)

Here foo, bar, and baz become applications that pipx exposes after installing the package, invoking their corresponding entry point functions.

        flowchart LR
    TOML["pyproject.toml<br/>[project.scripts]"] -->|defines| EP["entry points<br/>foo, bar"]
    EP -->|pipx install| VENV["isolated venv"]
    VENV -->|symlink or copy| BIN["~/.local/bin/<br/>foo, bar"]
    BIN -->|invokes| FUNC["my_package:<br/>main_func()"]

    classDef input fill:#3f72af,stroke:#28517f,color:#fff
    classDef proc fill:#2a9d8f,stroke:#1c6b61,color:#fff
    classDef venv fill:#7c4dff,stroke:#5a2fd0,color:#fff
    classDef out fill:#388e3c,stroke:#256128,color:#fff
    classDef fn fill:#c78c20,stroke:#8a6011,color:#fff
    class TOML input
    class EP proc
    class VENV venv
    class BIN out
    class FUNC fn
    

The pipx.run entry point group

When a user runs pipx run PACKAGE, pipx looks for a console script matching the package name. If the package name and script name differ, the user has to write pipx run --spec PACKAGE SCRIPT, which is less convenient.

Package authors can declare a pipx.run entry point group to tell pipx which function to invoke for pipx run. This entry point takes priority over console scripts when present.

[project.entry-points."pipx.run"]
my-package = "my_package.cli:main"
[options.entry_points]
pipx.run =
    my-package = my_package.cli:main

With this declaration, pipx run my-package invokes my_package.cli:main even if no console script named my-package exists. The build package uses this pattern so that pipx run build works even though build names its console script pyproject-build. The uv backend hands pipx run build to uv tool run, which reads console scripts only, so this needs --backend pip there (see Use the uv backend).

Detect a pipx installation

pipx records the environment name in pipx_metadata.json for packages installed with pipx install. Package code can read this name when it needs to give pipx-specific recovery instructions:

import json
import sys
from pathlib import Path


def pipx_environment() -> str | None:
    try:
        metadata = json.loads((Path(sys.prefix) / "pipx_metadata.json").read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return None
    environment = metadata.get("environment") if isinstance(metadata, dict) else None
    return environment if isinstance(environment, str) else None

Use the returned name as the target for commands such as pipx inject ENVIRONMENT 'mytool[feature]'. pipx records the extra on the main package, so reinstalling the environment retains it.

pipx run may create internal metadata without setting environment, so check the field value. Treat other keys as internal pipx state. If the function returns None, show the package’s usual installer-neutral advice.

Manual pages

To provide documentation via man pages on UNIX-like systems, ship them as data files. pipx exposes them the same way it exposes apps.

# pyproject.toml
[tool.setuptools.data-files]
"share/man/man1" = [
  "manpage.1",
]
# setup.cfg
[options.data_files]
share/man/man1 =
    manpage.1
# setup.py
setup(
    # other arguments here...
    data_files=[("share/man/man1", ["manpage.1"])],
)

Warning

The setuptools documentation calls the data-files keyword “discouraged”, but it has no alternative when you need to ship man pages.

# pyproject.toml
[tool.pdm.build]
source-includes = ["share"]

[tool.pdm.build.wheel-data]
data = [
  { path = "share/man/man1/*", relative-to = "." },
]

After installing the package, man manpage opens manpage.1. For a real-world example, see pycowsay’s setup.py.

Shell completions

A package can ship the completion script for its own command the same way it ships a man page. pipx picks up the three directories a wheel installs completion scripts into and links each one under PIPX_COMPLETION_DIR (default ~/.local/share), so the completions arrive with the package and leave with it.

Shipped by the package

Exposed to

Loaded by

share/bash-completion/completions/

~/.local/share/bash-completion/completions/

bash

share/fish/vendor_completions.d/

~/.local/share/fish/vendor_completions.d/

fish

share/zsh/site-functions/

~/.local/share/zsh/site-functions/

zsh (with the directory on fpath)

Declare them through the data files of your build backend, exactly as with man pages:

setup(
    name="your-tool",
    data_files=[
        ("share/bash-completion/completions", ["completions/your-tool"]),
        ("share/zsh/site-functions", ["completions/_your-tool"]),
        ("share/fish/vendor_completions.d", ["completions/your-tool.fish"]),
    ],
)

See Shell completions for how each shell loads these directories.

Read more about entry points in the setuptools quickstart.