From ac6d6a1304001f4563bc080f2cc118272eb9782d Mon Sep 17 00:00:00 2001 From: Ed Morley <501702+edmorley@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:49:15 +0000 Subject: [PATCH] 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-17595555. --- CHANGELOG.md | 4 ++ src/main.rs | 24 ---------- src/python_version.rs | 8 ++-- tests/django_test.rs | 14 +++--- .../.python-version | 2 +- .../requirements.txt | 5 +- .../pip_oldest_python/.python-version | 2 +- .../poetry_oldest_python/.python-version | 2 +- .../fixtures/poetry_oldest_python/poetry.lock | 4 +- .../poetry_oldest_python/pyproject.toml | 2 +- tests/fixtures/python_3.9/.python-version | 1 - tests/fixtures/python_3.9/requirements.txt | 0 .../python_version_eol/.python-version | 2 +- .../runtime.txt | 1 - .../fixtures/uv_oldest_python/.python-version | 2 +- .../fixtures/uv_oldest_python/pyproject.toml | 2 +- tests/fixtures/uv_oldest_python/uv.lock | 2 +- tests/pip_test.rs | 14 +++--- tests/poetry_test.rs | 14 +++--- tests/python_version_test.rs | 47 +++---------------- tests/uv_test.rs | 14 +++--- 21 files changed, 53 insertions(+), 113 deletions(-) delete mode 100644 tests/fixtures/python_3.9/.python-version delete mode 100644 tests/fixtures/python_3.9/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 188274d..7cbe4f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- Removed support for Python 3.9. ([#488](https://github.com/heroku/buildpacks-python/pull/488)) + ## [3.0.2] - 2026-01-05 ### Changed diff --git a/src/main.rs b/src/main.rs index 5adb475..04a0af7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,30 +94,6 @@ impl Buildpack for PythonBuildpack { } if let RequestedPythonVersion { - major: 3, - minor: 9, - origin, - .. - } = &requested_python_version - { - log_warning( - "Support for Python 3.9 is ending soon", - formatdoc! {" - Python 3.9 reached its upstream end-of-life on 31st October 2025, - and so no longer receives security updates: - https://devguide.python.org/versions/#supported-versions - - As such, support for Python 3.9 will be removed from this - buildpack on 7th January 2026. - - Upgrade to a newer Python version as soon as possible, by - changing the version in your {origin} file. - - For more information, see: - https://devcenter.heroku.com/articles/python-support#supported-python-versions - "}, - ); - } else if let RequestedPythonVersion { major: 3, minor: 10, origin, diff --git a/src/python_version.rs b/src/python_version.rs index c9ccac5..7236bf7 100644 --- a/src/python_version.rs +++ b/src/python_version.rs @@ -16,12 +16,11 @@ pub(crate) const DEFAULT_PYTHON_VERSION: RequestedPythonVersion = RequestedPytho #[cfg(test)] pub(crate) const DEFAULT_PYTHON_FULL_VERSION: PythonVersion = LATEST_PYTHON_3_14; -pub(crate) const OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION: u16 = 9; +pub(crate) const OLDEST_SUPPORTED_PYTHON_3_MINOR_VERSION: u16 = 10; pub(crate) const NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION: u16 = 14; pub(crate) const NEXT_UNRELEASED_PYTHON_3_MINOR_VERSION: u16 = NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION + 1; -pub(crate) const LATEST_PYTHON_3_9: PythonVersion = PythonVersion::new(3, 9, 25); pub(crate) const LATEST_PYTHON_3_10: PythonVersion = PythonVersion::new(3, 10, 19); pub(crate) const LATEST_PYTHON_3_11: PythonVersion = PythonVersion::new(3, 11, 14); pub(crate) const LATEST_PYTHON_3_12: PythonVersion = PythonVersion::new(3, 12, 12); @@ -173,7 +172,6 @@ pub(crate) fn resolve_python_version( (3, NEXT_UNRELEASED_PYTHON_3_MINOR_VERSION.., _) | (4.., _, _) => Err( ResolvePythonVersionError::UnknownVersion(requested_python_version.clone()), ), - (3, 9, None) => Ok(LATEST_PYTHON_3_9), (3, 10, None) => Ok(LATEST_PYTHON_3_10), (3, 11, None) => Ok(LATEST_PYTHON_3_11), (3, 12, None) => Ok(LATEST_PYTHON_3_12), @@ -284,13 +282,13 @@ mod tests { fn read_requested_python_version_python_version_file() { assert_eq!( read_requested_python_version( - Path::new("tests/fixtures/python_3.9"), + Path::new("tests/fixtures/python_3.13"), PackageManager::Pip ) .unwrap(), RequestedPythonVersion { major: 3, - minor: 9, + minor: 13, patch: None, origin: PythonVersionOrigin::PythonVersionFile, } diff --git a/tests/django_test.rs b/tests/django_test.rs index db3ba12..adfd8a0 100644 --- a/tests/django_test.rs +++ b/tests/django_test.rs @@ -49,8 +49,7 @@ fn django_staticfiles_latest_django() { ); } -// This tests the oldest Django version that works on Python 3.9 (which is the -// oldest Python that is available on all of our supported builders). +// This tests the oldest Django version that works on Python 3.10 (our oldest supported Python version). #[test] #[ignore = "integration test"] fn django_staticfiles_legacy_django() { @@ -63,7 +62,7 @@ fn django_staticfiles_legacy_django() { indoc! {" [Generating Django static files] Running 'manage.py collectstatic' - {'CPATH': '/layers/heroku_python/venv/include:/layers/heroku_python/python/include/python3.9:/layers/heroku_python/python/include:/invalid', + {'CPATH': '/layers/heroku_python/venv/include:/layers/heroku_python/python/include/python3.10:/layers/heroku_python/python/include:/invalid', 'DJANGO_SETTINGS_MODULE': 'testproject.settings', 'LC_CTYPE': 'C.UTF-8', 'LD_LIBRARY_PATH': '/layers/heroku_python/venv/lib:/layers/heroku_python/pip/lib:/layers/heroku_python/python/lib:/invalid', @@ -81,11 +80,10 @@ fn django_staticfiles_legacy_django() { ['/workspace', '/invalid', - '/layers/heroku_python/python/lib/python39.zip', - '/layers/heroku_python/python/lib/python3.9', - '/layers/heroku_python/python/lib/python3.9/lib-dynload', - '/layers/heroku_python/venv/lib/python3.9/site-packages'] - Copying '/workspace/testapp/static/robots.txt' + '/layers/heroku_python/python/lib/python310.zip', + '/layers/heroku_python/python/lib/python3.10', + '/layers/heroku_python/python/lib/python3.10/lib-dynload', + '/layers/heroku_python/venv/lib/python3.10/site-packages'] 1 static file copied to '/workspace/staticfiles'. "} diff --git a/tests/fixtures/django_staticfiles_legacy_django/.python-version b/tests/fixtures/django_staticfiles_legacy_django/.python-version index bd28b9c..c8cfe39 100644 --- a/tests/fixtures/django_staticfiles_legacy_django/.python-version +++ b/tests/fixtures/django_staticfiles_legacy_django/.python-version @@ -1 +1 @@ -3.9 +3.10 diff --git a/tests/fixtures/django_staticfiles_legacy_django/requirements.txt b/tests/fixtures/django_staticfiles_legacy_django/requirements.txt index e258a24..7f5462e 100644 --- a/tests/fixtures/django_staticfiles_legacy_django/requirements.txt +++ b/tests/fixtures/django_staticfiles_legacy_django/requirements.txt @@ -1,3 +1,2 @@ -# This is the oldest Django version that works on Python 3.9 (which is the -# oldest Python that is available on all of our supported builders). -Django==1.8.19 +# This is the oldest Django version that works on Python 3.10 (our oldest supported Python version). +Django==2.1.15 diff --git a/tests/fixtures/pip_oldest_python/.python-version b/tests/fixtures/pip_oldest_python/.python-version index c750569..5813e41 100644 --- a/tests/fixtures/pip_oldest_python/.python-version +++ b/tests/fixtures/pip_oldest_python/.python-version @@ -1,3 +1,3 @@ # This uses the .0 patch version of the oldest major Python version we support, # so we can check that the pip bootstrapping works with the oldest bundled pip. -3.9.0 +3.10.0 diff --git a/tests/fixtures/poetry_oldest_python/.python-version b/tests/fixtures/poetry_oldest_python/.python-version index a5c4c76..30291cb 100644 --- a/tests/fixtures/poetry_oldest_python/.python-version +++ b/tests/fixtures/poetry_oldest_python/.python-version @@ -1 +1 @@ -3.9.0 +3.10.0 diff --git a/tests/fixtures/poetry_oldest_python/poetry.lock b/tests/fixtures/poetry_oldest_python/poetry.lock index 502045e..e8576ce 100644 --- a/tests/fixtures/poetry_oldest_python/poetry.lock +++ b/tests/fixtures/poetry_oldest_python/poetry.lock @@ -13,5 +13,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "3b8ea94b618b2c3590bcbb224f2e2c723e6c14c8da90ba6917ca4a4fdcb716cf" +python-versions = "^3.10" +content-hash = "cfec7fa4755d6e9851dbc7848a76babbc6c288c7f13cc2bed2e60f83783d4fd2" diff --git a/tests/fixtures/poetry_oldest_python/pyproject.toml b/tests/fixtures/poetry_oldest_python/pyproject.toml index c69fd92..e193f69 100644 --- a/tests/fixtures/poetry_oldest_python/pyproject.toml +++ b/tests/fixtures/poetry_oldest_python/pyproject.toml @@ -2,5 +2,5 @@ package-mode = false [tool.poetry.dependencies] -python = "^3.9" +python = "^3.10" typing-extensions = "*" diff --git a/tests/fixtures/python_3.9/.python-version b/tests/fixtures/python_3.9/.python-version deleted file mode 100644 index bd28b9c..0000000 --- a/tests/fixtures/python_3.9/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.9 diff --git a/tests/fixtures/python_3.9/requirements.txt b/tests/fixtures/python_3.9/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/tests/fixtures/python_version_eol/.python-version b/tests/fixtures/python_version_eol/.python-version index cc1923a..bd28b9c 100644 --- a/tests/fixtures/python_version_eol/.python-version +++ b/tests/fixtures/python_version_eol/.python-version @@ -1 +1 @@ -3.8 +3.9 diff --git a/tests/fixtures/runtime_txt_and_python_version_file/runtime.txt b/tests/fixtures/runtime_txt_and_python_version_file/runtime.txt index f72c511..e69de29 100644 --- a/tests/fixtures/runtime_txt_and_python_version_file/runtime.txt +++ b/tests/fixtures/runtime_txt_and_python_version_file/runtime.txt @@ -1 +0,0 @@ -python-3.9.0 diff --git a/tests/fixtures/uv_oldest_python/.python-version b/tests/fixtures/uv_oldest_python/.python-version index a5c4c76..30291cb 100644 --- a/tests/fixtures/uv_oldest_python/.python-version +++ b/tests/fixtures/uv_oldest_python/.python-version @@ -1 +1 @@ -3.9.0 +3.10.0 diff --git a/tests/fixtures/uv_oldest_python/pyproject.toml b/tests/fixtures/uv_oldest_python/pyproject.toml index 08b9a31..bf4f995 100644 --- a/tests/fixtures/uv_oldest_python/pyproject.toml +++ b/tests/fixtures/uv_oldest_python/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "uv-oldest-python" version = "0.0.0" -requires-python = "==3.9.*" +requires-python = "==3.10.*" dependencies = [ "typing-extensions", ] diff --git a/tests/fixtures/uv_oldest_python/uv.lock b/tests/fixtures/uv_oldest_python/uv.lock index 1200f84..4787375 100644 --- a/tests/fixtures/uv_oldest_python/uv.lock +++ b/tests/fixtures/uv_oldest_python/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 2 -requires-python = "==3.9.*" +requires-python = "==3.10.*" [[package]] name = "typing-extensions" diff --git a/tests/pip_test.rs b/tests/pip_test.rs index 871725a..06025e6 100644 --- a/tests/pip_test.rs +++ b/tests/pip_test.rs @@ -237,15 +237,15 @@ fn pip_oldest_python() { context.pack_stdout, indoc! {" [Determining Python version] - Using Python version 3.9.0 specified in .python-version + Using Python version 3.10.0 specified in .python-version - [Warning: Support for Python 3.9 is ending soon] - Python 3.9 reached its upstream end-of-life on 31st October 2025, - and so no longer receives security updates: + [Warning: Support for Python 3.10 is deprecated] + Python 3.10 will reach its upstream end-of-life in October 2026, + at which point it will no longer receive security updates: https://devguide.python.org/versions/#supported-versions - As such, support for Python 3.9 will be removed from this - buildpack on 7th January 2026. + As such, support for Python 3.10 will be removed from this + buildpack on 6th January 2027. Upgrade to a newer Python version as soon as possible, by changing the version in your .python-version file. @@ -255,7 +255,7 @@ fn pip_oldest_python() { [Installing Python] - Installing Python 3.9.0 + Installing Python 3.10.0 "} ); }); diff --git a/tests/poetry_test.rs b/tests/poetry_test.rs index 62f1acd..3d97ffc 100644 --- a/tests/poetry_test.rs +++ b/tests/poetry_test.rs @@ -241,15 +241,15 @@ fn poetry_oldest_python() { context.pack_stdout, &formatdoc! {" [Determining Python version] - Using Python version 3.9.0 specified in .python-version + Using Python version 3.10.0 specified in .python-version - [Warning: Support for Python 3.9 is ending soon] - Python 3.9 reached its upstream end-of-life on 31st October 2025, - and so no longer receives security updates: + [Warning: Support for Python 3.10 is deprecated] + Python 3.10 will reach its upstream end-of-life in October 2026, + at which point it will no longer receive security updates: https://devguide.python.org/versions/#supported-versions - As such, support for Python 3.9 will be removed from this - buildpack on 7th January 2026. + As such, support for Python 3.10 will be removed from this + buildpack on 6th January 2027. Upgrade to a newer Python version as soon as possible, by changing the version in your .python-version file. @@ -259,7 +259,7 @@ fn poetry_oldest_python() { [Installing Python] - Installing Python 3.9.0 + Installing Python 3.10.0 [Installing Poetry] Installing Poetry {POETRY_VERSION} diff --git a/tests/python_version_test.rs b/tests/python_version_test.rs index 8c44ff4..c37ad7d 100644 --- a/tests/python_version_test.rs +++ b/tests/python_version_test.rs @@ -1,6 +1,6 @@ use crate::python_version::{ - DEFAULT_PYTHON_FULL_VERSION, DEFAULT_PYTHON_VERSION, LATEST_PYTHON_3_9, LATEST_PYTHON_3_10, - LATEST_PYTHON_3_11, LATEST_PYTHON_3_12, LATEST_PYTHON_3_13, LATEST_PYTHON_3_14, + DEFAULT_PYTHON_FULL_VERSION, DEFAULT_PYTHON_VERSION, LATEST_PYTHON_3_10, LATEST_PYTHON_3_11, + LATEST_PYTHON_3_12, LATEST_PYTHON_3_13, LATEST_PYTHON_3_14, NEWEST_SUPPORTED_PYTHON_3_MINOR_VERSION, PythonVersion, }; use crate::tests::default_build_config; @@ -29,12 +29,6 @@ fn python_version_unspecified() { }); } -#[test] -#[ignore = "integration test"] -fn python_3_9() { - builds_with_python_version("tests/fixtures/python_3.9", &LATEST_PYTHON_3_9); -} - #[test] #[ignore = "integration test"] fn python_3_10() { @@ -75,33 +69,7 @@ fn builds_with_python_version(fixture_path: &str, python_version: &PythonVersion TestRunner::default().build(default_build_config(fixture_path), |context| { assert_empty!(context.pack_stderr); - if major == 3 && minor == 9 { - assert_contains!( - context.pack_stdout, - &formatdoc! {" - [Determining Python version] - Using Python version {major}.{minor} specified in .python-version - - [Warning: Support for Python 3.9 is ending soon] - Python 3.9 reached its upstream end-of-life on 31st October 2025, - and so no longer receives security updates: - https://devguide.python.org/versions/#supported-versions - - As such, support for Python 3.9 will be removed from this - buildpack on 7th January 2026. - - Upgrade to a newer Python version as soon as possible, by - changing the version in your .python-version file. - - For more information, see: - https://devcenter.heroku.com/articles/python-support#supported-python-versions - - - [Installing Python] - Installing Python {major}.{minor}.{patch} - "} - ); - } else if major == 3 && minor == 10 { + if major == 3 && minor == 10 { assert_contains!( context.pack_stdout, &formatdoc! {" @@ -154,9 +122,8 @@ fn builds_with_python_version(fixture_path: &str, python_version: &PythonVersion # Check that the Python binary is using its own 'libpython' and not the system one: # https://github.com/docker-library/python/issues/784 - # Note: This has to handle Python 3.9 and older not being built in shared library mode. - libpython_path=$(ldd /layers/heroku_python/python/bin/python | grep libpython || true) - if [[ -n "${libpython_path}" && "${libpython_path}" != *"=> /layers/"* ]]; then + libpython_path=$(ldd /layers/heroku_python/python/bin/python | grep libpython) + if [[ "${libpython_path}" != *"=> /layers/"* ]]; then echo "The Python binary is not using the correct libpython!" echo "${libpython_path}" exit 1 @@ -326,14 +293,14 @@ fn python_version_eol() { [Determining Python version] [Error: The requested Python version has reached end-of-life] - Python 3.8 has reached its upstream end-of-life, and is + Python 3.9 has reached its upstream end-of-life, and is therefore no longer receiving security updates: https://devguide.python.org/versions/#supported-versions As such, it's no longer supported by this buildpack: https://devcenter.heroku.com/articles/python-support#supported-python-versions - Please upgrade to at least Python 3.9 by changing the + Please upgrade to at least Python 3.10 by changing the version in your .python-version file. If possible, we recommend upgrading all the way to Python {DEFAULT_PYTHON_VERSION}, diff --git a/tests/uv_test.rs b/tests/uv_test.rs index 9ed8975..b874d63 100644 --- a/tests/uv_test.rs +++ b/tests/uv_test.rs @@ -242,15 +242,15 @@ fn uv_oldest_python() { context.pack_stdout, &formatdoc! {" \\[Determining Python version\\] - Using Python version 3.9.0 specified in .python-version + Using Python version 3.10.0 specified in .python-version - \\[Warning: Support for Python 3.9 is ending soon\\] - Python 3.9 reached its upstream end-of-life on 31st October 2025, - and so no longer receives security updates: + \\[Warning: Support for Python 3.10 is deprecated\\] + Python 3.10 will reach its upstream end-of-life in October 2026, + at which point it will no longer receive security updates: https://devguide.python.org/versions/#supported-versions - As such, support for Python 3.9 will be removed from this - buildpack on 7th January 2026. + As such, support for Python 3.10 will be removed from this + buildpack on 6th January 2027. Upgrade to a newer Python version as soon as possible, by changing the version in your .python-version file. @@ -260,7 +260,7 @@ fn uv_oldest_python() { \\[Installing Python\\] - Installing Python 3.9.0 + Installing Python 3.10.0 \\[Installing uv\\] Installing uv {UV_VERSION}