What the end of life of Python 3.10 changes
The Python developer's guide publishes the schedule of every branch. Python 3.10, released in October 2021, is listed there as end of life since October 1, 2026. In practice the branch no longer receives any fix, not even security ones: a flaw found in the interpreter or its standard library will stay open.
Your code keeps running and your tests keep passing, which is what makes the situation misleading. End of life breaks nothing on the day itself. It removes the safety net for every following day, while distributions, base images and third-party libraries gradually lose interest.
What a frozen interpreter costs
- Unfixed flaws — any interpreter vulnerability published after end of life stays open on the 3.10 branch; only a version upgrade closes it.
- Dependencies drifting away — libraries sooner or later raise their minimum Python version; a fresh install ends up resolving old versions, or failing.
- Frozen base images — a container image built on the 3.10 branch relies on an interpreter with no upstream fixes, and your vulnerability scans flag it on every build.
- Stalled audits — an unsupported interpreter is a red line in your clients' security reviews and vendor questionnaires.
- Forced migration — from 3.10 to 3.12 the gap spans two minor versions; the longer you wait, the longer the list of breakages, and a planned project costs less than an emergency after an incident.
Python branch support calendar
Scroll the table
| Version | End of support | What it means for your VPS |
|---|---|---|
| 3.9 | October 2025 | Unsupported for over a year: leave without waiting |
| 3.10 | October 2026 | Unsupported since October 1: no more fixes |
| 3.11 | October 2027 | Minimum step: it fixes today's problem, not next year's |
| 3.12 | October 2028 | Recommended target: two years of margin and an established ecosystem |
| 3.13 | October 2029 | Supported even longer; adopt it once your dependencies are validated |
| 3.14 | October 2030 | Newest branch in this table, with the furthest end of support |
Which target version to choose
Python 3.12 is the target we recommend for an existing application: its end of support is scheduled for October 2028, two years of margin, and mainstream frameworks support it. Python 3.11 is a minimum step that puts you back in front of the same migration in October 2027.
Python 3.13 is supported even longer, until October 2029. We do not consider it unsuitable: it only assumes that each of your dependencies publishes packages for that version, which you can check in your test environment. A reminder from the official guide: the dates of branches that are still active are scheduled and can be adjusted, only those of 3.9 and 3.10 are settled. Reread the page before fixing your calendar.
Knowing which interpreter really serves production
python3 --version in your shell does not tell you which interpreter serves the application. The service may start a virtual environment created years ago, a hand-compiled Python, or an image frozen in a FROM python:3.10.
List every source: the ExecStart line of the systemd unit (systemctl cat myapp), the output of /srv/myapp/venv/bin/python --version, a grep -rn "FROM python" . over your Dockerfiles, the requires-python field of pyproject.toml and the version used by your CI. Also note workers and scheduled jobs: they often have their own environment and stay on the old interpreter long after the website.
Prerequisites before you start
This migration needs no special machine, but it needs full access to the server and room for two environments side by side: the second takes as much disk as the first, plus a build cache if a dependency has no precompiled package.
You need a locked dependency file with exact versions, a test suite you can run outside production, a recent database backup and root access to install, if your compiled dependencies require it, a compiler and the development headers. A first pass on a test VPS, even a short-lived one, beats a trial on the production server.
Migrating without interrupting the service
Freeze what exists
From the current environment, export the exact versions:
/srv/myapp/venv/bin/pip freeze > requirements.lock.txt. This file describes precisely what runs today; it is your way back. Commit it before going further.Install Python 3.12 without touching the system
uv python install 3.12downloads a 3.12 interpreter managed by uv, without replacing the distribution'spython3that its own tools depend on. If you prefer your distribution's packages, install the 3.12 branch from a repository that provides it and usepython3.12 -m venv.Create the second environment next to the first
uv venv --python 3.12 /srv/myapp/venv312creates the new environment; without uv,python3.12 -m venv /srv/myapp/venv312does the same job. The old one keeps serving traffic: nothing is modified. A virtual environment cannot be moved, which is why the new one has a different name.Reinstall from the locked file
VIRTUAL_ENV=/srv/myapp/venv312 uv pip sync requirements.lock.txtinstalls exactly the listed versions. With pip:/srv/myapp/venv312/bin/pip install -r requirements.lock.txt. If a pinned version does not exist for 3.12 or refuses to build, the installation stops here: that is the point, you find the breakage before production. Note every package to upgrade.Replay the test suite
Run the tests with the new interpreter:
/srv/myapp/venv312/bin/python -m pytest, or/srv/myapp/venv312/bin/python manage.py testfor Django, preceded bymanage.py check. Read the summary of deprecation warnings: they announce the breakages of the next version. Tests must pass with both interpreters before you continue.Upgrade only what broke
List the packages at fault, upgrade them one by one, then regenerate the locked file from the new environment:
/srv/myapp/venv312/bin/pip freeze > requirements.312.txt. Keep both lists, one per interpreter, for the duration of the switchover.Start the application in parallel on a local port
Run it with the new environment on a free port, for example
/srv/myapp/venv312/bin/gunicorn myapp.wsgi --bind 127.0.0.1:8001(oruvicornfor FastAPI). Send it real requests withcurl: home page, login, a database write. Public traffic stays on the old process; only an identical result justifies moving to the switchover.
Switching over and rolling back in one line
The switchover must change a single path. Point the systemd unit at a link rather than at the environment itself: ExecStart=/srv/myapp/venv-active/bin/gunicorn myapp.wsgi --bind 127.0.0.1:8000. First create the link to the current state with ln -sfn /srv/myapp/venv /srv/myapp/venv-active, then systemctl daemon-reload and a restart that changes nothing.
The switch to 3.12 then becomes ln -sfn /srv/myapp/venv312 /srv/myapp/venv-active && systemctl restart myapp, and the rollback is the same line with the old path. For a containerized application, the lever is the base image: change FROM python:3.10-slim to FROM python:3.12-slim, rebuild with docker compose up -d --build and roll back by restoring the previous line.
In both cases, keep the old environment or image for a few days, and follow the logs with journalctl -u myapp -f during the first hour.
Breakage points to check
Between 3.10 and 3.12, breakage rarely comes from your code and almost always from what surrounds it. Three families come back: modules removed from the standard library, compiled dependencies and the minimum version of your frameworks. The Python 3.12 release notes list what disappears; read them with your locked file open next to them.
Checklist before the switchover
- Removed modules — Python 3.12 removes
distutils,asynchat,asyncoreandimp;grep -rnE "distutils|asyncore|asynchat|import imp" .finds your direct uses, the list of your dependencies reveals the indirect ones. - setuptools is no longer preinstalled — environments created by
venvno longer ship it in 3.12; a package that importspkg_resourceswithout declaring it fails until you runpip install setuptools. - Precompiled packages — compiled dependencies (database drivers, image processing, cryptography) must publish a package for 3.12;
pip install --only-binary=:all: -r requirements.lock.txtfails fast instead of starting a build. - Minimum framework versions — Django 4.2 supports Python 3.12 only from 4.2.8, and Django 5.0 and later accept it; for FastAPI, reread the Python classifiers of the exact version you pin.
- Deprecation warnings —
datetime.utcnow()anddatetime.utcfromtimestamp()are deprecated in 3.12; replace them withdatetime.now(datetime.UTC)anddatetime.fromtimestamp(ts, datetime.UTC). - Shared system libraries — the parallel environment isolates Python packages, not the OS libraries (the PostgreSQL client, for example): note them, a system update can change them under your feet.
Troubleshooting: the errors that keep coming back
These messages appear as soon as the tests first run under Python 3.12. Each has a known cause and a short remedy.
Observed message and remedy
- ModuleNotFoundError: No module named 'distutils' — the module left the standard library. Install
setuptoolsin the environment, which still provides a version of it, then upgrade the faulty dependency. - ModuleNotFoundError: No module named 'imp' —
impis removed,importlibreplaces it. If the import comes from a third-party package, upgrade that package to a version that no longer uses it. - ModuleNotFoundError: No module named 'pkg_resources' — setuptools is no longer installed by default in 3.12 venv environments.
pip install setuptoolsrestores the module until the faulty package stops needing it. - DeprecationWarning: datetime.datetime.utcnow() is deprecated — not an error but a signal: replace the call with a timezone-aware object.
- Failed building wheel for <package> — this package has no precompiled version for 3.12 and pip tries to build it. Upgrade it, or install a compiler and development headers if upgrading is impossible.
Pin the target, do not just migrate
Use the migration to fix the interpreter instead of enduring it: requires-python = ">=3.12" in pyproject.toml, a precise image tag rather than python:3, and the Python version in your CI matrix. Add 3.13 to that matrix as a mere observation: you will know in advance whether the next upgrade will be simple.
Making version upgrades a routine
A Python branch reaches end of life every year, in October. The method described here (parallel environment, locked file, tests, reversible switchover) depends on no specific version: it replays as is for 3.11 in October 2027 and for 3.12 in October 2028, according to the current calendar.
If your application is not yet on a server whose interpreter you control, our guide to deploying Django on a VPS covers the first deployment. The guides on the end of life of Node.js and PHP follow the same logic for the other runtimes of a fleet.