Python Upgrade Guide
Step-by-step instructions on keeping your Python version up to date.
The Necessity of Upgrades
Snakes shed their skin multiple times throughout their lives — not because the old skin is damaged, but because it no longer fits their growing bodies. The process, called ecdysis, is triggered by hormones and begins weeks before the actual molt. During this period, the snake's vision becomes clouded as a new layer of skin forms beneath the old one. The snake becomes vulnerable, often hiding until the molt is complete. Once finished, though, the snake emerges with restored vision, better movement, and renewed defenses against parasites and injury.
Python applications undergo a similar process during version upgrades. The old runtime hasn't necessarily failed — it simply no longer fits the application's needs for security patches, performance improvements, and modern language features. Like a snake preparing to molt, the upgrade process involves a period of reduced visibility where tests may fail and dependencies may break. But once complete, the application emerges with renewed performance, security, and access to ecosystem improvements.
Historically, upgrading a programming language's runtime was often viewed with the same enthusiasm as navigating a storm. The infamous transition from Python 2 to Python 3, for example, introduced pervasive string handling changes that left many development teams wrestling with encoding issues for months.
Strictly speaking, you do not have to upgrade Python immediately upon a new release — at least not if your application is stable and isolated. However, delaying upgrades indefinitely incurs a compounding technical debt. Older versions eventually stop receiving security patches, leaving applications vulnerable. Furthermore, every major Python release — such as 3.10, 3.11, 3.12, or 3.13 — introduces significant performance enhancements and new language features. We want our applications to be secure and performant; therefore, establishing a routine upgrade process is an essential part of durable programming.
Before we get into the mechanics of the upgrade, though, it is wise to ensure that your application's test suite is currently passing. Upgrading Python while dealing with preexisting failing tests makes it incredibly difficult to isolate which problems were caused by the new Python version. Additionally, before you use any of the commands in this guide, it's wise to ensure the latest 'known good' version of your code is committed to source control.
Choosing a Version Manager
There are several approaches to managing Python versions on a development machine; depending on your particular circumstances, one may be more useful than the others.
The first is pyenv, a popular choice for projects that primarily rely on Python. It is lightweight, does not override shell commands unnecessarily, and adheres to the Unix philosophy of doing one thing well.
The second is asdf, which manages multiple languages — Node.js, Ruby, and Python — through a single interface. This option typically makes more sense if you work on polyglot applications.
A third option is mise, a faster, Rust-based alternative to asdf with improved performance and user experience.
We will use pyenv for our examples, though the general concepts apply regardless of your choice.
The Upgrade Process
1.Update the Version Manager
First, we need to ensure our version manager knows about the latest Python releases. If we try to install a newly released Python version using an outdated version manager, it will likely fail.
For pyenv on macOS using Homebrew, we can update it like this:
$ brew upgrade pyenv Alternatively, if you are on a Debian-based system like Ubuntu and installed pyenv via git, you might update it by pulling the latest changes:
$ cd ~/.pyenv && git pull One may wonder: why do we need to update pyenv itself? pyenv contains the definitions and scripts for downloading and compiling each specific Python release. Without an updated pyenv, the tool remains unaware that newer versions — like 3.13.2 — even exist.
2.Install the New Python Version
With our tools updated, we can install the target Python version. Let's assume we want to install Python 3.13.2:
$ pyenv install 3.13.2
Downloading Python-3.13.2.tar.xz...
-> https://www.python.org/ftp/python/3.13.2/Python-3.13.2.tar.xz
Installing Python-3.13.2...
# ...snip...
Installed Python-3.13.2 to /Users/david/.pyenv/versions/3.13.2 This process downloads the source code and compiles it. I've abbreviated the above output for the sake of brevity. Depending on your machine, this might take a few minutes.
Additionally, note that the exact version numbers you see will likely vary — no doubt, shortly after you read this, a newer patch version than 3.13.2 will be available.
Of course, if you are using mise, the command would be mise install python@3.13.2 followed by mise use python@3.13.2.
3.Update Project Files
Next, we must instruct our project to use the newly installed version. Typically, this involves updating the .python-version file and potentially your pyproject.toml if you're using modern Python packaging.
Let's update the .python-version file first:
$ echo "3.13.2" > .python-version If you're using pyproject.toml, update the Python version requirement:
# pyproject.toml
[project]
requires-python = ">=3.13" 4.Recreate Virtual Environment
Now that the project specifies the new Python version, we need to recreate our virtual environment and reinstall dependencies. This is necessary because some packages with C extensions must be recompiled against the new Python environment.
$ rm -rf .venv
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install -r requirements.txt Once the installation completes, we can run our test suite to verify the application behaves correctly under the new Python version.
$ pytest If the tests pass, the upgrade is largely complete. If they fail, however, you will need to investigate. Common upgrade issues include removed or deprecated standard library modules, changes in behavior for str and bytes, or packages that have not yet been updated to support the new Python version.
For example, if you encounter an error about a missing module like ModuleNotFoundError: No module named 'imp', it is highly likely that a dependency needs to be updated to support the newer Python version. In those cases, updating the offending packages via pip install --upgrade <package_name> is typically the next step.