How to migrate from Python 3.10/3.11 to 3.13: Handling the Dual EOL and Missing PyPI Wheels
Audit Your Dependency Matrix and EOL Exposure
Map every dependency's supported Python range before you touch the runtime. A locked tree that passes on Python 3.10 will fail on Python 3.13 the moment a transitive package caps requires_python below it.
Python 3.10 reaches end of life on October 31, 2026, and libraries are already dropping it from test matrices. Because Python 3.11 is next in the queue, treat both as legacy branches you are retiring rather than intermediate stops.
Generate a full lockfile with your package manager and inspect the requires_python metadata for every transitive dependency. If any package caps below 3.13, you must upgrade it or fork it before switching runtimes. Direct dependencies are easy to audit; transitive ones are the silent killers that break CI after the runtime switch.
With pip, freeze the environment, install pipdeptree, and dump the Python requirement for each package:
pip freeze > requirements.lock
pip install pipdeptree
pipdeptree --json | jq '.[] | {package: .package_name, python: .requires_python}'
With uv, compile a lockfile against the target version to surface blockers early:
uv pip compile pyproject.toml -o requirements.lock --python-version 3.13
If the compile succeeds, your declared dependencies nominally support 3.13; if it fails, the error names the offending package and its requires_python ceiling. Do not assume absence of manylinux wheels is the only barrier-metadata caps are invisible until you enforce them. Fix the caps in your lockfile first, then validate the upgrade in a container running Python 3.13.
Fix Missing PyPI Wheels and Build Toolchain Lag
Bump your build targets to macOS 12+ and current manylinux tags, and run cibuildwheel in CI before you tag a release.
Python 3.13 shipped on October 7, 2024, but binary wheels on PyPI often trail behind because packaging ecosystems move at volunteer speed. If you publish wheels, note that PyPI now rejects macOS 11 deployment targets for Python 3.13 with HTTP 400 errors. Update your build configuration to target macOS 12+ and current manylinux tags, and verify every release with cibuildwheel in CI before tagging.
In pyproject.toml, raise the macOS deployment target:
[tool.cibuildwheel.macos]
environment = { MACOSX_DEPLOYMENT_TARGET = "12.0" }
Then configure a current manylinux image for Linux builds under the same [tool.cibuildwheel.linux] table.
Next, add a CI step that runs cibuildwheel across all platforms before any tag is pushed:
- name: Build wheels
run: pip install cibuildwheel && cibuildwheel --platform all
If the build fails for any platform, fix the matrix and bump the target rather than retrying the upload. This prevents broken releases from reaching PyPI and gives downstream users installable wheels for Python 3.13 as soon as the release is tagged.
Teams migrating from Python 3.10 or Python 3.11 should audit their own libraries first, because volunteer-driven packaging means your dependencies may not publish Python 3.13 wheels until their maintainers update similar build settings.
Handle PEP 594 Stdlib Removals
Audit your codebase for deprecated stdlib imports and replace them with maintained PyPI alternatives before upgrading, because Python 3.13 removes 20 stdlib modules under PEP 594 and any lingering import will raise ModuleNotFoundError at runtime.
Begin by running a static search across your own source and vendored dependencies. Use grep to find references to removed modules such as nntplib in your application code:
grep -rE 'import\s+nntplib' .
When grep returns hits, treat them as blockers. Remove the stdlib import from your code, then add a maintained third-party replacement to your environment. You can verify the old import path now fails under Python 3.13 with a quick smoke test:
# python3.13
import nntplib # ModuleNotFoundError: No module named 'nntplib'
Check your transitive dependencies as well, because a library you do not control may still import a removed module. If you maintain a requirements.txt or lockfile, scan installed packages for deprecated imports before deployment.
Selecting a replacement package requires extra caution. Because reusing former stdlib names on PyPI creates naming conflicts for installers, confirm that the distribution name you choose does not shadow the removed module. After swapping the import, update your lockfile and run your full test suite under Python 3.13 to ensure the alternative integrates correctly and no deprecated stdlib paths remain.
Update Cloud Runtimes and Compliance Posture
Migrate staging and production to Python 3.13 now by bumping container base images and serverless runtimes, because cloud providers are deprecating Python 3.10 ahead of its October 2026 EOL and compliance frameworks already flag EOL language runtimes as security risks.
Update your Dockerfile to a 3.13 base image and rebuild your application image:
FROM python:3.13-slim
Drop Python 3.10 from CI test matrices so pipelines do not reinstall the obsolete interpreter:
strategy:
matrix:
python-version: ["3.12", "3.13"]
For serverless functions, update the runtime field in infrastructure definitions before the provider retires the version:
runtime: python3.13
Audit your repositories for stale references with a single command:
grep -r "python3.10" terraform/ serverless/ docker/
The EU Cyber Resilience Act and similar frameworks mandate timely vulnerability handling for products with digital elements and increasingly penalize EOL language runtimes. Retaining Python 3.10 in production guarantees an emergency upgrade when the runtime is disabled or an audit fails. Remove it from every base image, CI matrix, and function config before the deprecation deadline forces a rushed migration.
Teams that delay this change often discover hidden dependencies only after the provider begins rejecting deploys. Schedule the change during your next sprint rather than waiting for the forced deprecation window.
Validate with a Staggered Production Rollout
Even after CI passes, deploy Python 3.13 to a single canary or worker queue before a fleet-wide rollout, and keep rollback images for your previous runtime available so you can revert without rebuilding artifacts. This staged exposure limits the blast radius of packaging gaps and stdlib removals that automated tests often miss.
Python 3.13 removes 20 stdlib modules under PEP 594, so a canary will surface ModuleNotFoundError in production logs before the error scales across your fleet. Platform-specific wheel failures also tend to appear only on target hardware; for example, macOS 11 builds for Python 3.13 can be rejected by PyPI or fail to install from source. A staggered rollout lets you catch these defects when they affect one host instead of every instance.
If you use a worker queue, pin one consumer to the new interpreter while leaving the rest on the old version. This pattern isolates task failures and prevents a bad wheel or removed module from stalling your entire pipeline:
# Abort fast if a removed stdlib module is still imported
try:
import nntplib # Removed in 3.13
except ModuleNotFoundError:
raise SystemExit("Aborting: deprecated stdlib module missing")
Deploy the new runtime to a single canary instance and stream its logs for import failures and missing native extensions:
kubectl set image deployment/app app=myregistry/app:3.13-canary
kubectl logs -f deployment/app | grep -E "ModuleNotFoundError|manylinux|macosx"
Keep the previous runtime image tagged and ready. Because cloud providers are deprecating Python 3.10 runtimes ahead of its October 2026 EOL, maintaining rollback images during the transition window prevents you from being stranded on an unsupported interpreter if the 3.13 deployment fails. Revert the workload immediately if exceptions spike:
kubectl set image deployment/app app=myregistry/app:3.11-last-known-good
FAQ
Can I stay on Python 3.10 after October 2026?
No. The Python Software Foundation will stop releasing security patches for Python 3.10 after October 31, 2026, and upstream libraries are already removing it from their test matrices. Staying on an EOL runtime exposes you to unpatched vulnerabilities and compliance flags.
Why does my package upload fail for Python 3.13 on macOS?
PyPI is rejecting wheels built against older macOS deployment targets for 3.13; macOS 11 builds in particular fail with 400 errors. Update your build pipeline to target macOS 12 or later and verify your manylinux tags are current.
Which stdlib modules were removed in 3.13?
PEP 594 removed 20 stdlib modules. If your code imports nntplib, it will now raise ModuleNotFoundError at runtime. Audit your imports and replace them with maintained PyPI alternatives, noting that reusing stdlib names on PyPI creates ecosystem conflicts.
Should I upgrade directly to 3.13 or stop at 3.12?
Python 3.13 is worth trying now, but expect toolchain lag because releases depend on volunteer labor across the ecosystem. If critical dependencies lack 3.13 wheels, a common approach is to stage a 3.12 intermediate migration while upstream maintainers publish updated artifacts.
How do I check if my PyPI dependencies support 3.13?
A common approach is to query the PyPI JSON API for each package's requires_python field, or attempt a resolution with pip install --dry-run --python-version 3.13 in a fresh virtualenv. This surfaces missing wheels and version conflicts before you change production runtimes.
References for further reading
Sources consulted while researching this guide, included so you can verify the details and go deeper. Listing them is not a claim that every line was independently fact-checked.
- Migrating to Python 3.13 | TestComplete Documentation
- When should you upgrade to Python 3.13? Migrating scripts to Python 3.13 in Workiva - Support Center
- Python 3.10 End of Life (October 2026): Security and Migration Guide
- Python version End of Life - Exercism
I packaged the setup above into a ready-to-use kit - Python 3.10/3.11 โ 3.13 Migration Pack: EOL Survival Kit - for anyone who'd rather copy-paste than wire it from scratch: https://unfairhq.gumroad.com/l/izeuv
Comments
No comments yet. Start the discussion.