Contributing
Thanks for your interest in contributing to pipx!
Everyone who interacts with the pipx project via codebase, issue tracker, chat rooms, or otherwise is expected to follow the PSF Code of Conduct.
Submitting changes
- Fork the GitHub repository.
- Make a branch off of
mainand commit your changes to it. - Add a changelog entry.
- Submit a pull request to the
mainbranch on GitHub, referencing an open issue.
Changelog entries
The CHANGELOG.md file is built by towncrier from news fragments in the
changelog.d/ directory. To add an entry, create a news fragment in that directory named {number}.{type}.md, where
{number} is the issue number, and {type} is one of feature, bugfix, doc, removal, or misc.
For example, if your issue number is 1234 and it's fixing a bug, then you would create changelog.d/1234.bugfix.md. PRs
can span multiple categories by creating multiple files: if you added a feature and deprecated/removed an old feature
for issue #5678, you would create changelog.d/5678.feature.md and changelog.d/5678.removal.md.
A changelog entry is meant for end users and should only contain details relevant to them. In order to maintain a consistent style, please keep the entry to the point, in sentence case, shorter than 80 characters, and in an imperative tone. An entry should complete the sentence "This change will ...". If one line is not enough, use a summary line in an imperative tone, followed by a description of the change in one or more paragraphs, each wrapped at 80 characters and separated by blank lines.
You don't need to reference the pull request or issue number in a changelog entry, since towncrier will add a link using the number in the file name. Similarly, you don't need to add your name to the entry, since that will be associated with the pull request.
Codebase Architecture
All source code lives under src/pipx/. The CLI entry point is main.py, which dispatches to command modules in
commands/.
flowchart TD
MAIN["main.py<br/>CLI parser + dispatch"] --> CMDS["commands/"]
CMDS --> INSTALL["install.py"]
CMDS --> RUN["run.py"]
CMDS --> UPGRADE["upgrade.py"]
CMDS --> INJECT["inject.py"]
CMDS --> LIST["list_packages.py"]
CMDS --> PIN["pin.py"]
CMDS --> INTERP_CMD["interpreter.py"]
CMDS --> OTHER["uninstall, uninject,<br/>reinstall, ensure_path,<br/>environment, run_pip"]
MAIN --> VENV["venv.py<br/>Venv + VenvContainer"]
MAIN --> PATHS["paths.py<br/>PIPX_HOME resolution"]
VENV --> SHARED["shared_libs.py<br/>shared pip venv"]
VENV --> INSPECT["venv_inspect.py<br/>metadata reader"]
VENV --> META["pipx_metadata_file.py<br/>JSON persistence"]
RUN --> SPEC["package_specifier.py<br/>PEP 508 parsing"]
RUN --> INTERP["interpreter.py<br/>Python resolution"]
INTERP --> STANDALONE["standalone_python.py<br/>download builds"]
style MAIN fill:#3f72af,color:#fff
style CMDS fill:#2a9d8f,color:#fff
style INSTALL fill:#2a9d8f,color:#fff
style RUN fill:#2a9d8f,color:#fff
style UPGRADE fill:#2a9d8f,color:#fff
style INJECT fill:#2a9d8f,color:#fff
style LIST fill:#2a9d8f,color:#fff
style PIN fill:#2a9d8f,color:#fff
style INTERP_CMD fill:#2a9d8f,color:#fff
style OTHER fill:#2a9d8f,color:#fff
style VENV fill:#7c4dff,color:#fff
style PATHS fill:#c78c20,color:#fff
style SHARED fill:#c78c20,color:#fff
style INSPECT fill:#7c4dff,color:#fff
style META fill:#7c4dff,color:#fff
style SPEC fill:#388e3c,color:#fff
style INTERP fill:#388e3c,color:#fff
style STANDALONE fill:#388e3c,color:#fff
Key modules
venv.py is the core abstraction. Venv wraps a single virtual environment (create, install, uninstall, upgrade) and
VenvContainer manages the collection under PIPX_HOME/venvs/. Both delegate pip operations to the shared libraries
venv managed by shared_libs.py.
paths.py resolves all directory locations from environment variables, platform defaults, and legacy fallback paths.
pipx_metadata_file.py serializes install options (spec, pip args, injected packages) to JSON inside each venv so that
upgrade and reinstall can reproduce the original install.
interpreter.py and standalone_python.py handle Python version resolution. When --fetch-missing-python is passed,
pipx downloads a standalone build from
python-build-standalone and caches it locally. The
pipx interpreter subcommands (list, prune, upgrade) manage these cached interpreters.
commands/run.py includes PEP 723 inline script metadata parsing. When you run a .py file, pipx scans for a
# /// script block, extracts TOML-declared dependencies, and installs them into a cached temporary venv.
Running pipx For Development
To develop pipx, either create a developer environment, or perform an editable
install:
python -m pip install -e .
python -m pipx --version
Running Tests
Setup
pipx uses tox for development, continuous integration testing, and various tasks.
tox defines environments in tox.toml which can be run with tox run -e ENV_NAME. Environment names can be listed
with tox list.
Install tox for pipx development:
python -m pip install --user tox
Tests are defined as tox environments. You can see all tox environments with
tox list
At the time of this writing, the output looks like this
default environments:
3.13 -> run tests with 3.13
3.12 -> run tests with 3.12
3.11 -> run tests with 3.11
3.10 -> run tests with 3.10
3.9 -> run tests with 3.9
lint -> run pre-commit on the codebase
docs -> build documentation
man -> build man page
Creating a developer environment
For developing the tool (and to attach to your IDE) we recommend creating a Python environment via tox run -e dev,
afterwards use the Python interpreter available under .tox/dev/bin/python.
Unit Tests
To run unit tests in Python 3.12, you can run
tox run -e 3.12
Tip
You can run a specific unit test by passing arguments to pytest, the test runner pipx uses:
tox run -e 3.9 -- -k EXPRESSION
EXPRESSION can be a test name, such as
tox run -e 3.9 -- -k test_uninstall
Coverage errors can usually be ignored when only running a subset of tests.
Running Unit Tests Offline
Running the unit tests requires a directory .pipx_tests/package_cache to be populated from a fixed list of package
distribution files (wheels or source files). If you have network access, tox automatically makes sure this directory
is populated (including downloading files if necessary) as a first step. Thus, if you are running the tests with network
access, you can ignore the rest of this section.
If, however, you wish to run tests offline without the need for network access, you can populate
.pipx_tests/package_cache yourself manually beforehand when you do have network access.
Lint Tests
Linting is done via pre-commit, setting it up and running it can be done via tox by typing:
tox run -e lint
Installing or injecting new packages in tests
If the tests are modified such that a new package / version combination is pipx installed or pipx injected that
wasn't used in other tests, then one must make sure it's added properly to the packages lists in
testdata/tests_packages.
To accomplish this:
- Edit
testdata/tests_packages/primary_packages.txtto add the new package(s) that you wish topipx installorpipx injectin the tests.
Then using Github workflows to generate all platforms in the Github CI:
- Manually activate the Github workflow: Create tests package lists for offline tests
- Download the artifact
listsand put the files from it intotestdata/tests_packages/
Finally, check-in the new or modified list files in the directory testdata/tests_packages
Testing pipx on Continuous Integration builds
Upon opening pull requests GitHub Actions will automatically trigger.
Building Documentation
pipx autogenerates API documentation, and also uses templates.
When updating pipx docs, make sure you are modifying the docs directory.
You can generate the documentation with
tox run -e docs
This will capture CLI documentation for any pipx argument modifications, as well as generate templates to the docs directory.
Releasing New pipx Versions
The release process for pipx is designed to be simple and fully automated with a single button press. The workflow automatically determines the next version based on changelog fragments, generates the changelog, creates the release commit, and publishes to PyPI.
Initiating a Release
Navigate to the Actions tab in the GitHub repository and select the Pre-release workflow. Click Run workflow
and choose the appropriate version bump strategy. The auto option intelligently determines whether a minor or patch
bump is needed by examining the types of changelog fragments present. If new features or removals exist, it performs a
minor version bump; otherwise, it increments the patch version. Alternatively, you can explicitly select major,
minor, or patch to control the version increment directly.
What Happens During Release
Once triggered, the pre-release workflow executes the scripts/release.py script which collects all changelog fragments
from the changelog.d/ directory and uses towncrier to generate the updated changelog. It then creates a release commit
with the message "Release {version}" and tags it with the version number. After running pre-commit hooks to ensure
formatting, both the commit and tag are pushed to the main branch.
The act of pushing a version tag (matching the pattern *.*.*) automatically triggers the main release workflow. This
workflow builds the project distribution files, publishes the package to PyPI using trusted publishing, creates a GitHub
release with auto-generated notes, and builds the zipapp using the minimum supported Python version before uploading it
to the GitHub release assets.
Version Calculation Examples
Starting from version 1.8.0, the version bump types produce the following results: auto with feature fragments
becomes 1.9.0, while auto with only bugfixes becomes 1.8.1. Selecting major explicitly jumps to 2.0.0, minor
moves to 1.9.0, and patch increments to 1.8.1. This automation eliminates the need for manual version management
and ensures consistency across releases.