Tutorial

Python 3.10 end of life: migrating your apps on a VPS

Development9 min read7 steps

Python 3.10 reached end of life in October 2026: no more security fixes are published for this branch. Your Django or FastAPI application still answers, though, and nothing tells you the interpreter is frozen. The risk builds up silently, and the migration looks heavier with every month of delay. Here is a method that leaves the existing setup untouched: a second Python 3.12 environment next to the first, locked and tested dependencies, then a switchover you can undo in one line.

Contents· What the end of life of Python 3.10 changes1/14
  1. 01What the end of life of Python 3.10 changes
  2. 02What a frozen interpreter costs
  3. 03Python branch support calendar
  4. 04Which target version to choose
  5. 05Knowing which interpreter really serves production
  6. 06Prerequisites before you start
  7. 07Migrating without interrupting the service
  8. 08Switching over and rolling back in one line
  9. 09Breakage points to check
  10. 10Checklist before the switchover
  11. 11Troubleshooting: the errors that keep coming back
  12. 12Observed message and remedy
  13. 13Pin the target, do not just migrate
  14. 14Making version upgrades a routine

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

VersionEnd of supportWhat it means for your VPS
3.9October 2025Unsupported for over a year: leave without waiting
3.10October 2026Unsupported since October 1: no more fixes
3.11October 2027Minimum step: it fixes today's problem, not next year's
3.12October 2028Recommended target: two years of margin and an established ecosystem
3.13October 2029Supported even longer; adopt it once your dependencies are validated
3.14October 2030Newest 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

  1. 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.

  2. Install Python 3.12 without touching the system

    uv python install 3.12 downloads a 3.12 interpreter managed by uv, without replacing the distribution's python3 that its own tools depend on. If you prefer your distribution's packages, install the 3.12 branch from a repository that provides it and use python3.12 -m venv.

  3. Create the second environment next to the first

    uv venv --python 3.12 /srv/myapp/venv312 creates the new environment; without uv, python3.12 -m venv /srv/myapp/venv312 does 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.

  4. Reinstall from the locked file

    VIRTUAL_ENV=/srv/myapp/venv312 uv pip sync requirements.lock.txt installs 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.

  5. 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 test for Django, preceded by manage.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.

  6. 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.

  7. 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 (or uvicorn for FastAPI). Send it real requests with curl: 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, asyncore and imp; 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 venv no longer ship it in 3.12; a package that imports pkg_resources without declaring it fails until you run pip 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.txt fails 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() and datetime.utcfromtimestamp() are deprecated in 3.12; replace them with datetime.now(datetime.UTC) and datetime.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 setuptools in the environment, which still provides a version of it, then upgrade the faulty dependency.
  • ModuleNotFoundError: No module named 'imp' — imp is removed, importlib replaces 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 setuptools restores 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.

A server to test the next version before switching over

A VPS with root access lets you build a second Python environment next to the first, validate the tests, then switch over. The choice of OS is free and capacity scales vertically; the VPS administration option takes over if you do not want to run the server yourself.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab