Skip to content

Commit 81673e4

Browse files
committed
Remove Python 3.9 support
Python 3.9 reached upstream EOL on 31st October 2025: https://devguide.python.org/versions/#supported-versions The Python version support policy is that supported versions follows the upstream EOL lifecycle: https://devcenter.heroku.com/articles/python-support#python-version-support-policy And Python 3.9 support has been deprecated since 8th January 2025: https://devcenter.heroku.com/changelog-items/3095 As such, builds of apps using Python 3.9 will now fail with an error message explaining they must be upgraded to a newer/supported Python version. Apps using Python 3.9 that aren't able to upgrade immediately will need to pin to an older buildpack version temporarily. GUS-W-17595553.
1 parent 2161b9b commit 81673e4

File tree

29 files changed

+112
-215
lines changed

29 files changed

+112
-215
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased]
44

5+
- Removed support for Python 3.9. ([#2005](https://github.com/heroku/heroku-buildpack-python/pull/2005))
56
- Adjusted the error message shown if SQLite headers aren't found during package installation. ([#2006](https://github.com/heroku/heroku-buildpack-python/pull/2006))
67

78
## [v326] - 2026-01-05

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,10 @@ The supported Python versions are:
5151
- Python 3.13
5252
- Python 3.12
5353
- Python 3.11
54-
- Python 3.10
5554

5655
These Python versions are deprecated on Heroku:
5756

58-
- Python 3.9
57+
- Python 3.10
5958

6059
Python versions older than those listed above are no longer supported, since they have reached
6160
end-of-life [upstream](https://devguide.python.org/versions/#supported-versions).

builds/build_python_runtime.sh

Lines changed: 23 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ function abort() {
2727
case "${STACK:?}" in
2828
heroku-22 | heroku-24)
2929
SUPPORTED_PYTHON_VERSIONS=(
30-
"3.9"
3130
"3.10"
3231
"3.11"
3332
"3.12"
@@ -58,10 +57,6 @@ case "${PYTHON_MAJOR_VERSION}" in
5857
SIGSTORE_IDENTITY='pablogsal@python.org'
5958
SIGSTORE_ISSUER='https://accounts.google.com'
6059
;;
61-
3.9)
62-
SIGSTORE_IDENTITY='lukasz@langa.pl'
63-
SIGSTORE_ISSUER='https://github.com/login/oauth'
64-
;;
6560
*)
6661
abort "Unsupported Python version '${PYTHON_MAJOR_VERSION}'!"
6762
;;
@@ -102,42 +97,34 @@ CONFIGURE_OPTS=(
10297
"--enable-optimizations"
10398
# Make autoconf's configure option validation more strict.
10499
"--enable-option-checking=fatal"
100+
# Shared builds are beneficial for a number of reasons:
101+
# - Reduces the size of the build, since it avoids the duplication between
102+
# the Python binary and the static library.
103+
# - Permits use-cases that only work with the shared Python library,
104+
# and not the static library (such as `pycall.rb` or `PyO3`).
105+
# - More consistent with the official Python Docker images and other distributions.
106+
#
107+
# Shared builds are slower unless `no-semantic-interposition`and LTO is used,
108+
# however, as of Python 3.10 `no-semantic-interposition` is enabled by default:
109+
# https://fedoraproject.org/wiki/Changes/PythonNoSemanticInterpositionSpeedup
110+
# https://github.com/python/cpython/issues/83161
111+
"--enable-shared"
105112
# Install Python into `/tmp/python` rather than the default of `/usr/local`.
106113
"--prefix=${INSTALL_DIR}"
107114
# Skip running `ensurepip` as part of install, since the buildpack installs a curated
108115
# version of pip itself (which ensures it's consistent across Python patch releases).
109116
"--with-ensurepip=no"
117+
"--with-lto"
118+
# Counter-intuitively, the static library is still generated by default even when
119+
# the shared library is enabled, so we disable it to reduce the build size.
120+
# This option only exists for Python 3.10+.
121+
"--without-static-libpython"
110122
)
111123

112-
if [[ "${PYTHON_MAJOR_VERSION}" != +(3.9) ]]; then
113-
CONFIGURE_OPTS+=(
114-
# Shared builds are beneficial for a number of reasons:
115-
# - Reduces the size of the build, since it avoids the duplication between
116-
# the Python binary and the static library.
117-
# - Permits use-cases that only work with the shared Python library,
118-
# and not the static library (such as `pycall.rb` or `PyO3`).
119-
# - More consistent with the official Python Docker images and other distributions.
120-
#
121-
# However, shared builds are slower unless `no-semantic-interposition`and LTO is used:
122-
# https://fedoraproject.org/wiki/Changes/PythonNoSemanticInterpositionSpeedup
123-
# https://github.com/python/cpython/issues/83161
124-
#
125-
# It's only as of Python 3.10 that `no-semantic-interposition` is enabled by default,
126-
# so we only use shared builds on Python 3.10+ to avoid needing to override the default
127-
# compiler flags.
128-
"--enable-shared"
129-
"--with-lto"
130-
# Counter-intuitively, the static library is still generated by default even when
131-
# the shared library is enabled, so we disable it to reduce the build size.
132-
# This option only exists for Python 3.10+.
133-
"--without-static-libpython"
134-
)
135-
fi
136-
137-
if [[ "${PYTHON_MAJOR_VERSION}" != +(3.9|3.10) ]]; then
124+
if [[ "${PYTHON_MAJOR_VERSION}" != +(3.10) ]]; then
138125
CONFIGURE_OPTS+=(
139126
# Skip building the test modules, since we remove them after the build anyway.
140-
# This feature was added in Python 3.10+, however it wasn't until Python 3.11
127+
# This feature was added in Python 3.10, however it wasn't until Python 3.11
141128
# that compatibility issues between it and PGO were fixed:
142129
# https://github.com/python/cpython/pull/29315
143130
"--disable-test-modules"
@@ -155,31 +142,14 @@ fi
155142
# - https://wiki.ubuntu.com/ToolChain/CompilerFlags
156143
# - https://wiki.debian.org/Hardening
157144
# - https://github.com/docker-library/python/issues/810
158-
# We only use `dpkg-buildflags` for Python versions where we build in shared mode (Python 3.9+),
159-
# since some of the options it enables interferes with the stripping of static libraries.
160-
if [[ "${PYTHON_MAJOR_VERSION}" == +(3.9) ]]; then
161-
EXTRA_CFLAGS=''
162-
LDFLAGS='-Wl,--strip-all'
163-
else
164-
EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)"
165-
LDFLAGS="$(dpkg-buildflags --get LDFLAGS) -Wl,--strip-all"
166-
fi
145+
EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)"
146+
LDFLAGS="$(dpkg-buildflags --get LDFLAGS) -Wl,--strip-all"
167147

168148
CPU_COUNT="$(nproc)"
169149
make -j "${CPU_COUNT}" "EXTRA_CFLAGS=${EXTRA_CFLAGS}" "LDFLAGS=${LDFLAGS}"
170150
make install
171151

172-
if [[ "${PYTHON_MAJOR_VERSION}" == +(3.9) ]]; then
173-
# On older versions of Python we're still building the static library, which has to be
174-
# manually stripped since the linker stripping enabled in LDFLAGS doesn't cover them.
175-
# We're using `--strip-unneeded` since `--strip-all` would remove the `.symtab` section
176-
# that is required for static libraries to be able to be linked.
177-
# `find` is used since there are multiple copies of the static library in version-specific
178-
# locations, eg:
179-
# - `lib/libpython3.9.a`
180-
# - `lib/python3.9/config-3.9-x86_64-linux-gnu/libpython3.9.a`
181-
find "${INSTALL_DIR}" -type f -name '*.a' -print -exec strip --strip-unneeded '{}' +
182-
elif ! find "${INSTALL_DIR}" -type f -name '*.a' -print -exec false '{}' +; then
152+
if ! find "${INSTALL_DIR}" -type f -name '*.a' -print -exec false '{}' +; then
183153
abort "Unexpected static libraries found!"
184154
fi
185155

@@ -220,7 +190,7 @@ LD_LIBRARY_PATH="${SRC_DIR}" "${SRC_DIR}/python" -m compileall -f --invalidation
220190
# (e.g. `python -m pydoc`) if needed.
221191
rm "${INSTALL_DIR}"/bin/{idle,pydoc}*
222192
# The 2to3 module and entrypoint was removed from the stdlib in Python 3.13.
223-
if [[ "${PYTHON_MAJOR_VERSION}" == +(3.9|3.10|3.11|3.12) ]]; then
193+
if [[ "${PYTHON_MAJOR_VERSION}" == +(3.10|3.11|3.12) ]]; then
224194
rm "${INSTALL_DIR}"/bin/2to3*
225195
fi
226196

lib/pip.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function pip::install_pip() {
2828
# them installed.
2929
# - Most of the Python ecosystem has stopped installing them for Python 3.12+ already.
3030
# See the Python CNB's removal for more details: https://github.com/heroku/buildpacks-python/pull/243
31-
if [[ "${python_major_version}" == +(3.9|3.10|3.11|3.12) ]]; then
31+
if [[ "${python_major_version}" == +(3.10|3.11|3.12) ]]; then
3232
build_data::set_string "setuptools_version" "${SETUPTOOLS_VERSION}"
3333
build_data::set_string "wheel_version" "${WHEEL_VERSION}"
3434
packages_to_install+=(

lib/pipenv.sh

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@ function pipenv::install_pipenv() {
4545
local bundled_pip_module_path
4646
bundled_pip_module_path="$(utils::bundled_pip_module_path "${python_home}" "${python_major_version}")"
4747

48-
# We must call the venv Python directly here, rather than relying on pip's `--python`
49-
# option, since `--python` was only added in pip v22.3, so isn't supported by the older
50-
# pip versions bundled with Python 3.9/3.10.
5148
# `--isolated`: Prevents any custom pip configuration added by third party buildpacks (via env
5249
# vars or global config files) from breaking package manager bootstrapping.
5350
# shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled.

lib/poetry.sh

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,8 @@ function poetry::install_poetry() {
5151
local bundled_pip_module_path
5252
bundled_pip_module_path="$(utils::bundled_pip_module_path "${python_home}" "${python_major_version}")"
5353

54-
# We must call the venv Python directly here, rather than relying on pip's `--python`
55-
# option, since `--python` was only added in pip v22.3, so isn't supported by the older
56-
# pip versions bundled with Python 3.9/3.10.
5754
# `--isolated`: Prevents any custom pip configuration added by third party buildpacks (via env
5855
# vars or global config files) from breaking package manager bootstrapping.
59-
# We pin to an older dulwich version to work around https://github.com/jelmer/dulwich/issues/1948
60-
# on Python 3.9.0/3.9.1. TODO: Remove this pin when we drop support for Python 3.9 in Jan 2026.
6156
# shellcheck disable=SC2310 # This function is invoked in an 'if' condition so set -e will be disabled.
6257
if ! {
6358
"${poetry_venv_dir}/bin/python" "${bundled_pip_module_path}" \

lib/python_version.sh

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
# however, it helps Shellcheck realise the options under which these functions will run.
55
set -euo pipefail
66

7-
LATEST_PYTHON_3_9="3.9.25"
87
LATEST_PYTHON_3_10="3.10.19"
98
LATEST_PYTHON_3_11="3.11.14"
109
LATEST_PYTHON_3_12="3.12.12"
1110
LATEST_PYTHON_3_13="3.13.11"
1211
LATEST_PYTHON_3_14="3.14.2"
1312

14-
OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION=9
13+
OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION=10
1514
NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION=14
1615

1716
DEFAULT_PYTHON_FULL_VERSION="${LATEST_PYTHON_3_14}"
@@ -525,7 +524,6 @@ function python_version::resolve_python_version() {
525524
# Otherwise map major version specifiers to the latest patch release.
526525
case "${requested_python_version}" in
527526
*.*.*) echo "${requested_python_version}" ;;
528-
3.9) echo "${LATEST_PYTHON_3_9}" ;;
529527
3.10) echo "${LATEST_PYTHON_3_10}" ;;
530528
3.11) echo "${LATEST_PYTHON_3_11}" ;;
531529
3.12) echo "${LATEST_PYTHON_3_12}" ;;
@@ -688,24 +686,7 @@ function python_version::warn_if_deprecated_major_version() {
688686
local requested_major_version="${1}"
689687
local version_origin="${2}"
690688

691-
if [[ "${requested_major_version}" == "3.9" ]]; then
692-
output::warning <<-EOF
693-
Warning: Support for Python 3.9 is ending soon!
694-
695-
Python 3.9 reached its upstream end-of-life on 31st October 2025,
696-
and so no longer receives security updates:
697-
https://devguide.python.org/versions/#supported-versions
698-
699-
As such, support for Python 3.9 will be removed from this
700-
buildpack on 7th January 2026.
701-
702-
Upgrade to a newer Python version as soon as possible, by
703-
changing the version in your ${version_origin} file.
704-
705-
For more information, see:
706-
https://devcenter.heroku.com/articles/python-support#supported-python-versions
707-
EOF
708-
elif [[ "${requested_major_version}" == "3.10" ]]; then
689+
if [[ "${requested_major_version}" == "3.10" ]]; then
709690
output::warning <<-EOF
710691
Warning: Support for Python 3.10 is deprecated!
711692
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.9
1+
3.10
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
# This is the oldest Django version that works on Python 3.9 (which is the
2-
# oldest Python that is available on all of our supported builders).
3-
Django==1.8.19
1+
# This is the oldest Django version that works on Python 3.10 (our oldest supported Python version).
2+
Django==2.1.15
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
python-3.9.0
1+
python-3.10.0

0 commit comments

Comments
 (0)