A change in the handling of PYTHONPATH between Python 3.10 and 3.12

January 21, 2025

Our long time custom for installing Django for our Django based web application was to install it with 'python3 setup.py install --prefix /some/where', and then set a PYTHONPATH environment variable that pointed to /some/where/lib/python<ver>/site-packages. Up through at least Python 3.10 (in Ubuntu 22.04), you could start Python 3 and then successfully do 'import django' with this; in fact, it worked on different Python versions if you were pointing at the same directory tree (in our case, this directory tree lives on our NFS fileservers). In our Ubuntu 24.04 version of Python 3.12 (which also has the Ubuntu packaged setuptools installed), this no longer works, which is inconvenient to us.

(It also doesn't seem to work in Fedora 40's 3.12.8, so this probably isn't something that Ubuntu 24.04 broke by using an old version of Python 3.12, unlike last time.)

The installed site-packages directory contains a number of '<package>.egg' directories, a site.py file that I believe is generic, and an easy-install.pth that lists the .egg directories. In Python 3.10, strace says that Python 3 opens site.py and then easy-install.pth during startup, and then in a running interpreter, 'sys.path' contains the .egg directories. In Python 3.12, none of this happens, although CPython does appear to look at the overall 'site-packages' directory and 'sys.path' contains it, as you'd expect. Manually adding the .egg directories to a 3.12 sys.path appears to let 'import django' work, although I don't know if everything is working correctly.

I looked through the 3.11 and 3.12 "what's new" documentation (3.11, 3.12) but couldn't find anything obvious. I suspect that this is related to the removal of distutils in 3.12, but I don't know enough to say for sure.

(Also, if I use our usual Django install process, the Ubuntu 24.04 Python 3.12 installs Django in a completely different directory setup than in 3.10; it now winds up in <top level>/local/lib/python3.12/dist-packages. Using 'pip install --prefix ...' does create something where pointing PYTHONPATH at the 'dist-packages' subdirectory appears to work. There's also 'pip install --target', which I'd forgotten about until I stumbled over my old entry.)

All of this makes it even more obvious to me than before that the Python developers expect everyone to use venvs and anything else is probably going to be less and less well supported in the future. Installing system-wide is probably always going to work, and most likely also 'pip install --user', but I'm not going to hold my breath for anything else.

(On Ubuntu 24.04, obviously we'll have to move to a venv based Django installation. Fortunately you can use venvs with programs that are outside the venv.)


Comments on this page:

This is an... interesting breaking change on a minor version bump.

You probably know this already, but Ruby has gone the venv-ish way many many years ago, and it has worked well. I'm not sure how Python's venv works, but Bundler (Ruby's package manager) installs packages in a global-ish location (that is probably equivalent to pip install --user) and it will reuse anything that's already installed there (so 2 projects that have common dependencies will share those files.)

By Legooolas at 2025-01-24 09:42:07:

The straightforward answer with Python (for this and other recent changes like the PEP-668 environment changes which also affect pip install --user, as well as my previously preferred separated-installs-without-venvs hack of PYTHONUSERBASE=/some/shared/dir pip install --user <item>) is to always use venvs. Anything else is just too brittle now :(

By cks at 2025-01-24 12:08:43:

As a belated note: Python's versioning scheme is different than it looks. Python 3.10 and 3.12 are effectively semi-major versions, not 'minor' versions, because a hypothetical 'Python 4' would likely be a major and incompatible language change, much as Python 2 to Python 3 was.

(This particular approach to version numbers isn't unique to Python; I think it's common to many computer languages. Go works similarly, with major language evolutions introduced between 'Go 1.x' and 'Go 1.x+1', such as generics. Go 1.x only guarantees language and standard library API backward compatibility, not tooling backward compatibility, and they've made major changes over Go versions that are are even larger than this Python change.)

Written on 21 January 2025.
« The (potential) complexity of good runqueue latency measurement in Linux
More features for web page generation systems doing URL remapping »

Page tools: View Source, View Normal.
Search:
Login: Password:

Last modified: Tue Jan 21 22:40:55 2025
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.