Upgrade the Python Version of a Production Django App
Upgrade a production Django app to a newer Python version safely: check Django and dependency support, run tests on both versions in CI, rebuild virtualenvs or images, upgrade Celery workers together, deploy gradually, and roll back cleanly.
Problem statement
Each Python version receives security fixes for about five years, and newer versions bring real performance improvements. Running an end-of-life Python means no security patches and, eventually, dependencies that drop support. Upgrading a production Django app from, say, 3.10 to 3.13 touches every layer: Django's supported versions, compiled dependencies, the Docker base image or server packages, virtualenvs, Celery workers, and CI. Done as a deliberate, tested change, it is routine; done as a side effect of an OS upgrade, it breaks things.
- Check that your Django version supports the target Python (Django's FAQ lists supported versions) and upgrade Django first if needed.
- Update dependencies that lack wheels or support for the new version (
pip install --dry-runoruv lockagainst it). - Run CI on both the current and target Python versions until the switch.
- Fix deprecation warnings: run tests with
-W error::DeprecationWarningfor your own code. - Rebuild the Docker image on the new base (
python:3.13-slim) or create new virtualenvs; never upgrade in place. - Deploy web, Celery workers, and Beat on the new version together; keep the previous image or venv for rollback.
Step-by-step solution
1. Check compatibility
Django supports specific Python versions per release (for example Django 5.2 supports 3.10-3.13; check the current table). If your Django is older than the target Python, upgrade Django first, separately. Then check dependencies:
uv python install 3.13
uv venv /tmp/py313 --python 3.13
uv pip install --python /tmp/py313/bin/python -r requirements.txt
Failures usually come from old pins of compiled packages (psycopg2, lxml, numpy, Pillow, cryptography) without wheels for the new version, or packages using removed standard-library modules (Python 3.12 removed distutils, 3.13 removed several legacy modules such as cgi and telnetlib).
2. Test on both versions
strategy:
matrix:
python-version: ["3.11", "3.13"]
steps:
- uses: actions/setup-python@v5
with: { python-version: "${{ matrix.python-version }}" }
- run: pip install -r requirements.txt
- run: python -W error::DeprecationWarning -m pytest
Running both catches version-specific failures while keeping the current version deployable.
3. Rebuild, do not upgrade in place
- Docker: change the base image tag (
FROM python:3.13-slim) and rebuild; the old image remains your rollback. - Servers: install the new Python (distro package, deadsnakes, or
uv python install), create a new virtualenv at a versioned path, install the lock file, and switch a symlink.
Virtualenvs are tied to the interpreter that created them; see Fix a Broken Django Virtualenv After an OS Upgrade.
4. Upgrade every process together
Web, Celery workers, Beat, and scheduled commands should run the same Python. Celery messages are serialised as JSON by default, so mixed versions usually interoperate, but pickled data (if you use pickle for results or cache values) may not be compatible across versions. Clear or version caches that store pickled objects if you see deserialisation errors.
5. Deploy gradually and watch
Roll out to staging, then a canary or one server, and compare error rates and latency with the old version. Python 3.11+ is often noticeably faster for Django code; measure it so you can see the gain.
Verification check: python --version in every running process type (web, worker, beat) shows the new version, the test suite passes on it in CI, error rates after deploy match the previous version, and a rollback to the previous image has been rehearsed.
Explanation
Python minor versions change the bytecode, the C API that compiled extensions use, and the standard library (removing deprecated modules). Compiled packages therefore need builds for the new version, and pure-Python packages need to avoid removed APIs. Django declares the Python versions each release supports. Treating the interpreter as a dependency, tested in CI on both versions and deployed by rebuilding images or environments, turns the upgrade into a normal, reversible release rather than a risky in-place change.
Edge cases / notes
- Free-threaded builds (3.13t) and the experimental JIT are opt-in; most deployments use the standard build.
- Managed platforms set the Python version through a file (
.python-version,runtime.txt) or base image; update it there. - Lambda and serverless runtimes have their own supported Python lists.
- Type checkers and linters may need updates to understand new syntax.
Next steps
- Plan Django upgrades with the Django Upgrade Checklist.
- Manage Python versions with Deploy Django With uv.
FAQ
How do I upgrade Python for a Django project?
Check Django and dependency support, test on both versions in CI, rebuild the image or virtualenv, and deploy all processes together.
Which Python version does my Django version support?
Django's documentation FAQ lists supported Python versions for each release.
Can I upgrade Python in place on the server?
Avoid it. Create a new virtualenv or image with the new Python and switch to it.
Is a newer Python faster for Django?
Usually yes; 3.11 and later brought significant speed-ups. Measure on your workload.