Repository: beeware/briefcase
Pull request #2305 (2025-05-21 15:10:37)

Add warnings about Python versions past EOL date
Added a validation function for the system's python version, warning if it is past it's end of life date, based on the dates from here: https://devguide.python.org/versions/.

PR picks up where #1822 left off

Fixes https://github.com/beeware/briefcase/issues/1669


## PR Checklist:
- [x] All new features have been tested
- [x] All new features have been documented
- [x] I have read the **CONTRIBUTING.md** file
- [x] I will abide by the code of conduct



Patch:
diff --git a/changes/1669.misc.rst b/changes/1669.misc.rst
new file mode 100644
index 00000000..e482b7f2
--- /dev/null
+++ b/changes/1669.misc.rst
@@ -0,0 +1,1 @@
+Warnings about outdated Python versions will now be shown.
diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py
index 4a19b58c..0b184e4a 100644
--- a/src/briefcase/commands/base.py
+++ b/src/briefcase/commands/base.py
@@ -11,6 +11,7 @@ import subprocess
 import sys
 from abc import ABC, abstractmethod
 from argparse import RawDescriptionHelpFormatter
+from datetime import datetime
 from pathlib import Path
 from typing import Any
 
@@ -170,6 +171,7 @@ class BaseCommand(ABC):
             console=console,
             base_path=self.data_path / "tools",
         )
+        self.validate_python_version()
 
         # Immediately add tools that must be always available
         Subprocess.verify(tools=self.tools)
@@ -287,6 +289,36 @@ a custom location for Briefcase's tools.
 """
             )
 
+    def validate_python_version(self) -> bool:
+        """Validate the system's Python version is within its end-of-life date.
+
+        :return: True if the Python version is within its end-of-life date.
+        """
+        # Python EOL dates are provided on: https://devguide.python.org/versions/
+        # EOL for Python 3.8 was October 2024. Assuming Python releases occur on the
+        # same yearly cadence, the EOL for Python 3.x is October of 2024 + (x - 8)
+        EOL_year = 2024 + (sys.version_info.minor - 8)
+        if datetime.today() > datetime(EOL_year, 10, 1):
+            self.console.warning(
+                f"""
+*************************************************************************
+** WARNING: Your Python version is unsupported!                        **
+*************************************************************************
+
+    The version of Python you are using ({platform.python_version()}) is past its
+    end of life. As a result, it is highly likely your Briefcase
+    version is also out of date.
+
+    See https://devguide.python.org/versions/ for details on currently
+    supported Python versions.
+
+*************************************************************************
+"""
+            )
+            return False
+
+        return True
+
     def _command_factory(self, command_name: str):
         """Command factory for the current platform and format.
 


Test patch:
diff --git a/tests/commands/base/test_end_of_life.py b/tests/commands/base/test_end_of_life.py
new file mode 100644
index 00000000..04637c75
--- /dev/null
+++ b/tests/commands/base/test_end_of_life.py
@@ -0,0 +1,42 @@
+import sys
+from collections import namedtuple
+from datetime import datetime
+from unittest import mock
+
+import pytest
+
+import briefcase
+
+
+def _create_version_info(major, minor, patch=0):
+    Version = namedtuple("Version", "major minor patch")
+    return Version(major, minor, patch)
+
+
+@pytest.mark.parametrize(
+    "minor_version, today, is_valid",
+    [
+        (8, datetime(2025, 10, 1), False),  # after EOL
+        (9, datetime(2025, 10, 1), True),  # on EOL
+        (9, datetime(2025, 10, 2), False),  # after EOL
+        (10, datetime(2026, 9, 30), True),  # before EOL
+        (10, datetime(2026, 10, 2), False),  # after EOL
+        (14, datetime(2030, 9, 30), True),  # before EOL
+        (14, datetime(2030, 10, 2), False),  # after EOL
+    ],
+)
+def test_valid_python_version(
+    minor_version, today, is_valid, base_command, monkeypatch
+):
+    """A warning is produced if the Python version is past its EOL"""
+
+    version_info = _create_version_info(3, minor_version)
+    sys_mock = mock.MagicMock(wraps=sys)
+    sys_mock.version_info = version_info
+    monkeypatch.setattr(briefcase.commands.base, "sys", sys_mock)
+
+    datetime_mock = mock.MagicMock(wraps=datetime)
+    datetime_mock.today.return_value = today
+    monkeypatch.setattr(briefcase.commands.base, "datetime", datetime_mock)
+
+    assert base_command.validate_python_version() is is_valid