On July 28, 2024, the pip team released pip 24.2. As a recent addition to the pip core team, I’ll walk you through the noteworthy or interesting changes from pip 24.2. Let’s go!
PSA, PSA, PSA 📣
If you’re using setuptools, do not have a
pyproject.toml
, and use pip’s-e
flag, you may be impacted by the deprecation of legacy editable installs.If you’re an user of setuptools and came to this post after seeing the legacy editable install deprecation warning, you should first read the deprecation issue for solutions, not here.
You can come back and read this after for more historical context on the deprecation, however! It’s a good read, I promise.
Here’s a link to the 24.2 changelog if you’d like the full list of changes.
Legacy editable installs are deprecated
TL;DR, don’t panic. The
-e
option is not deprecated, but the way it works under the hood will change, potentially necessitating changes to how your project is packaged.
Over the last decade, there has been a major transition towards standardized mechanisms for packaging Python projects:
- PEP 517: introduced an interface where frontends (e.g. the pip installer) can interact with a build backend (e.g. Poetry1), including asking it to build a wheel for later installation
- PEP 518: introduced
pyproject.toml
and defined a standard format for declaring build-time dependencies using this file - PEP 621: defined a standard format for declaring project metadata in
pyproject.toml
- PEP 660: augmented the PEP 517 interface to define a way to ask a build backend to prepare an editable install
These PEPs are what enable the use of alternative backends like Poetry, Hatch, and
scikit-build-core without needing to add specific support for each backend in every
frontend (e.g. pip, tox, uv). Instead of running setup.py bdist_wheel
to ask setuptools
to build a wheel for installation, pip can simply call the build_wheel
hook2 from PEP 517 which all backends are guaranteed to have.
As each standard matured, pip has progressively deprecated and removed support for legacy (and often setuptools-specific) mechanisms to reduce technical debt and ensure that pip can continue to evolve.
Editable installs—the thing you get from pip install -e <path>
—used to be a setuptools
only feature, requiring pip to call the project’s setup.py
with the develop
sub-command. However, this stopped being the case under PEP 660. As long as your
project’s backend supports PEP 660, pip install -e
will continue to work. No setuptools
required. (Although modern setuptools works fine as well.)
OK, so what now?
This release,
pip has deprecated support for the setup.py develop
fallback
which is used when a project lacks support for modern editable installs. It will be
removed in pip 25.0 (Q1 2025) after which projects MUST support PEP 660 to perform an
editable install. Affected projects will see this deprecation warning:
pip install -e temp/pip-test-package
Obtaining file:///home/ichard26/dev/oss/pip/temp/pip-test-package
Preparing metadata (setup.py) ... done
Installing collected packages: version_pkg
DEPRECATION: Legacy editable install of version_pkg==0.1 from file:///home/ichard26/dev/oss/pip/temp/pip-test-package (setup.py develop) is deprecated. pip 25.0 will enforce this behaviour change. A possible replacement is to add a pyproject.toml or enable --use-pep517, and use setuptools >= 64. If the resulting installation is not behaving as expected, try using --config-settings editable_mode=compat. Please consult the setuptools documentation for more information. Discussion can be found at https://github.com/pypa/pip/issues/11457
Running setup.py develop for version_pkg
Successfully installed version_pkg-0.1
In practice, this usually means one of two things:
- The
pyproject.toml
file doesn’t exist at all, thus pip will opt-out of the PEP 517 interface, runningsetup.py
as required - The declared setuptools version (in the
[build-system].requires
field) is too old and doesn’t support PEP 660, i.e. anything older than setuptools 64.0.0
For affected projects, the best solution is likely to use a modern version of setuptools
and declare this via pyproject.toml
. You can place your project metadata in this file
as well, but it’s not required to stop the legacy editable deprecation warnings.
# pyproject.toml
[build-system]
requires = ["setuptools >= 64"]
build-backend = "setuptools.build_meta"
Tip: if you have other dependencies needed to run
setup.py
or otherwise build your package, you should add them to the["setuptools >= 64"]
list. This field is equivalent to setuptools’setup_requires
parameter.
Alternatively, projects can pass the --enable-pep517
flag to force pip to use the PEP
517 interface, including the editable install extensions from PEP 660. If no backend is
declared, pip will assume that the project uses setuptools and ensure it’s available in
the isolated build environment.3 As long as setuptools 64+ still supports your
Python version, a modern editable install will be performed. Once the legacy mechanism is
removed, --use-pep517
will have no effect and will essentially be enabled by default.
Finally, if you don’t like either of those solutions, you can always throw away setuptools and switch to a different backend entirely. I’ve mentioned a few earlier, but there are even more options to choose from if you’re willing to do some research and play around. I’d start with with the Python Packaging User Guide’s list of the commonly used backends if you want to replace setuptools.
If you stick with setuptools, one potential snag is that setuptools has introduced a new kind of editable installs while rolling out PEP 660 by default:
- If a package uses the “src” layout or otherwise places the source code in a separate
top-level directory, setuptools will create a static
.pth
file to extend the Python import path with that directory - If a package uses the “flat” layout, setuptools will install a custom import hook which
intercepts imports and “redirects” them to the original modules (so auxiliary top-level
files like
noxfile.py
orsetup.py
or atests
package aren’t importable)
If the legacy behaviour is desired, one must pass
--config-settings editable_mode=compat
. There is also one more method:
strict editable installs. They behave closer to a normal installation, but are
implemented in an entirely different way from setup.py
or the default method described
above.4
(Update: a setuptools maintainer reached out to strict editable installs are not enabled by default. It turns out that the situation behind potential breakage is a bit more nuanced. Sorry!)
Warning: Static analysis tools including mypy, pyright, and pylint may not function properly when setuptools uses an import hook to implement an editable install. This is a known issue. The recommended workaround is to pass
--config-settings editable_mode=compat
.
For more details using setuptools with pyproject.toml
, you should read their
documentation:
- ⭐ User guide to using
pyproject.toml
⭐ - ⭐ setuptools’ editable install docs ⭐
- setuptools’ build system docs
If you’d like to learn more, especially about the history of why and how these standards came to exist, I strongly recommend reading Paul Ganssle’s excellent “Why you shouldn’t invoke setup.py directly” article. It’s long, but packs way more information than I could ever hope to cover.
System HTTPS certificates by default(*)
If you’ve ever used pip in a corporate environment, there is a good chance that you’ve encountered a network or SSL verification error because the remote index server (PyPI or a private index) returned a HTTPS certificate that couldn’t be verified.
pip install toto
Looking in indexes: https://repos.company/api/pypi/python_pypi/simple
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(0, 'unknown error (_ssl.c:3630)'),)': /api/pypi/python_pypi/simple/toto/
WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(0, 'unknown error (_ssl.c:3630)'),)': /api/pypi/python_pypi/simple/toto/
WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(0, 'unknown error (_ssl.c:3630)'),)': /api/pypi/python_pypi/simple/toto/
WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(0, 'unknown error (_ssl.c:3630)'),)': /api/pypi/python_pypi/simple/toto/
WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError(SSLError(0, 'unknown error (_ssl.c:3630)'),)': /api/pypi/python_pypi/simple/toto/
Could not fetch URL https://repos.company/api/pypi/python_pypi/simple/toto/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host= 'repos.company', port=443): Max retries exceeded with url: /api/pypi/python_pypi/simple/toto/ (Caused by SSLError(SSLError(0, 'unknown error (_ssl.c:3630)'),)) - skipping
ERROR: Could not find a version that satisfies the requirement toto (from versions: none)
ERROR: No matching distribution found for toto
The problem is that while your system may be configured with the required certificate authorities, pip had no ability to use those system CAs. This resulted in a confusing user experience, because while your browser was likely able to access the index server, pip blew up with a non-obvious error.
To be able to verify HTTPS certificates, since the early days, pip has shipped with the
certifi CA bundle, which is simply a Python repackaging of the Mozilla CA bundle. To get
pip to verify HTTPS certificates not trusted by certifi, you had to pass your own bundle
via the --cert
option which is clunky.
In pip 22.2, pip gained the ability to use truststore for HTTPS certificate validation,
enabling pip to use the system certificate store instead of solely relying on certifi. The
integration was experimental; users had to have truststore already installed and opt-in
via --use-feature=truststore
, but it was a step forward.5 The plan was to enable
truststore by default once the feature matured.
Two years later, in this pip release, truststore is now enabled by default.6 Thus, assuming your system is configured properly with the required CAs, pip should simply “just work” even in many corporate environments. 🎉
There is one major problem though—you saw that asterisk, right? Truststore only works on
Python 3.10 or higher. pip will continue to exclusively use its certifi bundle on Python
3.9 or 3.8. Additionally,
any Python implementation that does not implement the ssl
module APIs needed by truststore
will not be able to use the system trust store either. In these situations, you will need
to continue using --cert
.
Major thanks goes to @sethmlarson for co-authoring truststore and writing patches improving and fixing pip’s truststore integration!
Performance optimizations, duh!
In this release, several optimizations of the environment inspection, file download, dependency resolution, and installation logic landed. While individually they are minor optimizations, together they provide a noticeable performance uplift in a variety of workloads!
Faster discovery of installed packages
First of all, on Python 3.11 or higher, pip is significantly faster to discover installed
packages (especially if there are a lot of ’em). This not only makes pip list
faster,
but also makes pip generally faster as it has to figure out what packages are already
installed quite frequently throughout its operation.
These are the before and after runtimes of pip list
in my pip development environment
which has 75 packages installed:
# before: pip list (24.1)
real 0m0.227s
user 0m0.192s
sys 0m0.035s
# after: pip list (24.2)
real 0m0.207s
user 0m0.170s
sys 0m0.036s
On slower systems, the performance improvement will be greater.7 This
uplift was achieved by optimizing pip’s importlib
based metadata backend to read
package8 names and versions from the installed metadata directory
names.9 Previously, pip would read the name and version from the
METADATA
file, which is slow as it involves an extra file read and invoking an email
header parser.
Other optimizations
- Faster downloads PR #12810 @morotti
- Source distributions and wheels are downloaded in 256 KiB chunks instead of 10 KiB chunks, which significantly reduces overhead and enables faster download speeds. A sizable chunk (🥁) of the performance improvement comes from reducing the number of updates of the download progress bar.
- Faster dependency resolution PR #12663 @notatallshaw
- … of highly complex or pathological dependency trees. As pip’s dependency resolver
explores a dependency tree, it has to parse requirements (e.g.
pip==24.2
) into a format it understands. In complex trees, the same requirements are often encountered many, many times. There was some caching in-place to minimize redundant work, but now requirements are consistently cached. - Faster installation PR #12803 @morotti
- Wheels, which are really zip archives, are extracted using a larger block size (either
1MiB or the file size if it’s smaller than a MiB). Also, the zip decompressor is no
longer invoked while copying empty files, eliminating pointless CPU cycles for files
like
__init__.py
. - Faster reinstalls PR #12755 yours truly!
- The full list of platform compatibility tags is now only generated once and reused across all look-ups of the wheel cache, speeding up cached reinstalls.
pip check
just got a bit stricter
The check
pip command now verifies whether installed packages are declared to support
the current platform, issuing a warning if a package is incompatible.
pip check
catboost 1.1.1 is not supported on this platform
ninja 1.11.1.1 is not supported on this platform
xgboost 1.6.1 is not supported on this platform
frozendict 2.3.8 is not supported on this platform
Under the hood, for every package, pip reads the WHEEL
metadata file that’s installed
alongside the package, and checks whether at least one of the compatibility tags is
supported on the platform.
For example, this is the WHEEL
file from pip 24.2’s wheel:
# pip-24.2.dist-info/WHEEL
Wheel-Version: 1.0
Generator: setuptools (71.1.0)
Root-Is-Purelib: true
Tag: py3-none-any
py3-none-any
is a platform compatibility tag. What’s a compatibility tag?
They’re essentially markers for what systems a package supports. You can
think of them like languages. They include information on the architecture, Python ABI,
and Python version. Every system has a set of compatibility tags it supports. If a package
and the system it’s being installed to share a common tag—or with the analogy, speak a
shared language—the package is considered compatible.
In this case, py3-none-any
is code for “any Python 3 version”, “any Python ABI”, and
“any architecture.” This is supported by any modern Python installation, thus this wheel
would be almost always eligible for installation (although it may be rejected for other
reasons, like requires-python
restrictions).
Now, pip install
already uses compatibility tags to ensure it only installs compatible
packages. This extra check in pip check
is designed to surface packages that were
compatible at the time of installation, but are no longer supported due to a system
upgrade. This is possible, especially as
Apple transitions from the x86-64
architecture (Intel) to arm64
(Apple Silicon).
(Un)fortunately, this has had the side-effect of revealing numerous packages with inaccurate or outright malformed metadata, leading to false positives. This is annoying, but regarded as an intentional side-effect as the pip project has been moving towards enforcing standards compliance so pip follows the standards instead of de facto writing them.10
This was kindly contributed by @q0w.
Oh yeah, --require-virtualenv
is a thing
You can configure pip to only function when running in an activated virtual environment
via the --require-virtualenv
flag. You can set this via a system-side or user-side
configuration file; and it’s great for ensuring you don’t scribble all over your system or
user Python environment.
pip install package-that-shouldnt-be-installed-system-wide
ERROR: Could not find an activated virtualenv (required).
Anyway, it’s supposed to only apply to commands that modify the environment, like
pip install
or pip uninstall
. Read-only commands, such as pip list
, are unaffected
by the flag and will function even if a virtual environment is not activated.
This release extends this QoL feature to pip check
and pip index
. You can now discover
unsupported packages whenever you want. Thanks @branchvincent!
Summary
pip 24.2 delivers considerable performance and QoL improvements. It also continues the pip project’s goal of following and enforcing the use of and compliance with the interoperatability standards that have shaped modern Python packaging.
I realize that this post is long, but this release contained some cool changes and I believed they deserved more attention than a brief entry in the changelog. I make no promises that I’ll continue this series for future pip releases, but this sure was fun to write!
Finally, I’d like to thank Bartosz Sokorski, Bradley Reynolds, Hugo van Kemenade, and Paul Moore for reviewing this post in draft form and suggesting improvements. Any mistakes are of course my own.
Poetry is the all encompassing user-facing tool for packaging and dependency management, while
poetry-core
is its backend. The same nitpick exists for all other “backends” I mention. ↩︎In reality, pip calls other hooks as well to query additional build dependencies and generate metadata, but this isn’t the post where I explain pip’s entire PEP 517 implementation. ↩︎
--no-build-isolation
may be needed if the project has build-time requirements beyond setuptools and wheel. By passing this flag, you are responsible for making sure your environment already has the required dependencies to build your package. ↩︎As of writing, the TL;DR is that strict editables are implemented as a tree of file links to the original source files in an auxiliary directory which is then added to
sys.path
, while legacy “compat mode” editables are implemented by placing the entire root of your package onsys.path
. The new method is designed to ensure only files present at installation are exposed, mimicking a normal installation. It’s rather neat that setuptools has support for all three methods described for implementing an editable install in PEP 660. ↩︎This was recognized as a problem in 2014, and pip gained the ability to use system certificates on Unix in 2015, but this change was later reverted because it turned out none of the major distributions had a functional OpenSSL configuration at the time. ↩︎
pip continues to use truststore in tandem with certifi. This is necessary for platforms that do not support truststore, but also provides a fallback if the system trust store is broken or otherwise inaccessible for some reason. There are currently no plans for pip to switch to exclusively use truststore. ↩︎
Also, it’s worth mentioning that 3/4 of the 200ms is taken by Python’s own startup overhead and importing everything
pip list
needs. ↩︎OK, so technically I should be saying “distribution” to avoid ambiguity as the term “package” means something different under the Python import system, but distribution sounds weird which is why I’m saying “package” still. I’m horribly inconsistent about this though. ↩︎
If you dig into your
site-packages
directory, you’ll notice that there is a bunch of<name>-<version>.dist-info
directories. These contain package metadata, and there is one for each package installed. They are included in wheels and are copied during installation. ↩︎Historically, as pip was the only installer in town, various bits of pip’s design or behaviour has been become de facto standards. Even if there is a standard, “pip supports X” or “pip does X” can be regularly seen in the issue trackers of other packaging tools. While this is expected and unavoidable, it’s unideal. We don’t want to be in the business of telling what everyone else should be doing, especially as pip is old and has quirks and design flaws that frankly shouldn’t exist, let alone be copied by other tools. By gradually enforcing standards compliance, the Python packaging ecosystem becomes more and more defined by common specifications, allowing other packaging tools to function without reimplementing a bunch of pip bugs or whatever. ↩︎