Your package declares requires-python = ">=3.13". A contributor who works
on Python 3.14 adds a template string, and guards it with a version check:
import sys
name = "Ada"
print("starting")
if sys.version_info >= (3, 14):
message = t"Hello {name}"
else:
message = f"Hello {name}"
On Python 3.13, this file fails with SyntaxError, and starting never
prints. Python parses the whole file before it runs any of it, so the if
never runs. If you support a Python version, every line of your package has
to run on it. This article shows how to check that.
Know when each feature arrived
| Feature | First Python |
|---|---|
except* |
3.11 |
The type statement, and def first[T](...) generics |
3.12 |
copy.replace, typing.TypeIs, typing.ReadOnly |
3.13 |
Type parameter defaults, as in class Box[T = int] |
3.13 |
Template strings, as in t"Hello {name}" |
3.14 |
Each kind of feature needs a different approach on an older Python.
- Syntax must parse on the oldest version. To use newer syntax, put it in a separate module and import that module only on newer Pythons. Each branch is then one more path to test.
- Library functions can sometimes come from a backport or a small fallback you write. Check that the replacement behaves the same, including its exceptions.
- Typing features often have backports in the
typing_extensionspackage, which gives older Pythons objects such asTypeIsandReadOnly. It cannot add syntax.typing_extensions.TypeVar("T", default=int)works on 3.12, butclass Box[T = int]still needs 3.13.
See the typing_extensions documentation.
Make the metadata and the tools agree
For a project whose oldest Python is 3.14, put these settings in its
pyproject.toml:
[project]
requires-python = ">=3.14"
[tool.mypy]
python_version = "3.14"
[tool.ruff]
target-version = "py314"
requires-python tells installers which Pythons you support. python_version makes mypy report
syntax and standard library names that the oldest version lacks.
target-version stops Ruff from suggesting rewrites that need a newer
Python. Neither tool runs your code on that version, so CI still has to.
mypy checks only the branch of an if sys.version_info >= ... test that
matches its target, and it skips the other branch. If your code has such
branches, run mypy once for each target. See
mypy's python_version
and Ruff's target-version.
Check your understanding
A library supports Python 3.14 and newer. Can it use the type statement and except*?
Show answer
Hide answer
Yes. The type statement arrived in Python 3.12 and except* in 3.11, so
both work on every version the library supports. Tests on the oldest
version still have to show that the code itself works.
Test the oldest and the newest
Run your tests on the oldest Python you support and on the newest. Add the
versions in between, and other operating systems, where your users need
them. Build the wheel, install it in a fresh environment and check that the
installed package works, as the CI article showed. Also check that the installed package
reports the Requires-Python you meant, with
importlib.metadata.metadata("mypkg")["Requires-Python"].
Raise the minimum on purpose
Installers use Requires-Python to skip releases that do not support the
running Python. Say version 2 of your package requires Python 3.15, and
version 1 supports 3.14. pip on Python 3.14 then installs version 1. If the
user asked for version 2 exactly, or another package needs version 2, the
install fails instead. The field does nothing for someone who copies your
source files.
Check your understanding What should go with a release that raises the minimum Python? Show answer Hide answer
Update requires-python, the mypy and Ruff targets, the CI versions and the
documentation in the same release. Say so in the release notes, and name
the last release that still supports the old minimum. Raise the minimum
before you ship code that needs it. You can also raise it only to stop
supporting an old Python, as long as you tell your users.
See the Requires-Python specification.