Upgrading Python 3.10 to 3.13: what breaks and how to fix it

By Ryan Murdoc, founder of Unstale · Published 17 October 2026 · Last checked October 17, 2026 · 6 min read

Python 3.10 reached end of life on October 1, 2026. Version 3.10.22 was the last release. New security holes in 3.10 itself will not be fixed.

Python 3.10 no longer gets security fixes. Your app keeps running, but new security holes in 3.10 itself will not be patched, and libraries are dropping 3.10 one release at a time. Container images and hosting platforms follow soon after.

You can go from 3.10 to 3.13 in one step. Most of the work is in your dependencies, not your own code. This guide is built from the official "What's New" notes for Python 3.11, 3.12 and 3.13, and lists the changes that actually stop apps running, plus the ones that change behaviour without an error.

Pick your target

Each Python version gets security fixes for five years from its release. This guide targets 3.13. If you pick 3.14, the same steps apply, with one more set of removals to check.

VersionSecurity fixes untilChoose it when
3.1231 Oct 2028A dependency you can't replace doesn't support 3.13 yet
3.1331 Oct 2029The safe default for most apps today
3.1431 Oct 2030Every dependency already supports it

Before you start

  1. Find every place 3.10 is pinned. Check Dockerfile base images (python:3.10-slim), CI matrices, .python-version, runtime.txt, requires-python in pyproject.toml, tox.ini, and your hosting platform's runtime setting. Miss one and production runs a different Python from your tests.
  2. Update dependencies on 3.10 first. Move each package to its newest release that still supports 3.10 and fix what breaks. Then the Python change is the only thing that changes in the next step.
  3. Turn deprecation warnings into errors in tests. Run your suite with python -W error::DeprecationWarning (or pytest's -W error::DeprecationWarning). Almost everything removed in 3.11 to 3.13 warned first.

Dependencies: where most upgrades get stuck

Packages with compiled code need a release built for the new Python. If an old pin has no wheel for 3.13, pip tries to compile it from source, which fails in most slim containers.

  • NumPy: 2.1.0 is the first release with Python 3.13 support. Going from NumPy 1.x to 2.x is an upgrade in its own right, so move pandas, SciPy and anything else built on NumPy at the same time.
  • Django: 4.2 stops at Python 3.12. For 3.13 you need 5.1.3 or later, and 5.2 LTS is the version to choose.
  • Anything pinned for years (common ones are cryptography, psycopg2, lxml, Pillow and grpcio): move to a release that publishes wheels for 3.13.

A quick test: create a fresh 3.13 virtual environment and run pip install -r requirements.txt. Every package that fails to install goes on your list.

Removals that crash (the easy ones)

These raise ImportError or AttributeError, so tests find them, as long as the code path is tested.

RemovedUse insteadRemoved in
@asyncio.coroutineasync def3.11
inspect.getargspec(), inspect.formatargspec()inspect.signature()3.11
open() with mode "U"Drop the "U"; universal newlines are the default3.11
distutilssetuptools, or packaging for version parsing3.12
impimportlib3.12
asyncore, asynchat, smtpdasyncio; aiosmtpd for SMTP servers3.12
assertEquals, failUnless, assertRaisesRegexp and other unittest aliasesassertEqual, assertTrue, assertRaisesRegex3.12
configparser.SafeConfigParser, readfp()ConfigParser, read_file()3.12
locale.format()locale.format_string()3.12
cgi, cgitb, crypt, imghdr, pipes, telnetlib, uu and 12 moreSee the note below3.13
lib2to3 and the 2to3 toolNothing; delete the code that uses them3.13
unittest.makeSuite(), findTestCases(), getTestCaseNames()The matching unittest.TestLoader methods3.13
typing.io, typing.reImport the names straight from typing3.13

Python 3.13 removed 19 old standard library modules in one go (PEP 594). The ones web apps hit most are cgi (form and header parsing), imghdr (image type checks) and crypt (password hashes). PEP 594 lists a replacement for each. Search your dependencies as well as your own code: old libraries import these modules too.

The silent ones: changes with no error

These cause the worst bugs, because nothing fails. The app runs and does something slightly different.

String enums format differently (3.11)

If you define enums like class Status(str, Enum), f-strings and format() now give the member name instead of the value. That quietly changes URLs, cache keys, log lines, and anything written to a database or API.

# class Status(str, Enum): ACTIVE = "active"
f"/users?status={Status.ACTIVE}"
# Python 3.10: /users?status=active
# Python 3.13: /users?status=Status.ACTIVE

# Fix: use StrEnum (3.11+), or .value
class Status(StrEnum):
    ACTIVE = "active"

Enums based on IntEnum, IntFlag and StrEnum are not affected. Search for (str, Enum) and (int, Enum).

setuptools is no longer in new virtual environments (3.12)

Since 3.12, venv does not install setuptools. Code that imports pkg_resources works on your laptop and fails wherever environments are built fresh, such as CI and new servers. Add setuptools to your dependencies, or better, move to importlib.metadata:

# Before
import pkg_resources
v = pkg_resources.get_distribution("mypackage").version

# After
from importlib.metadata import version
v = version("mypackage")

Invalid escape sequences now warn (3.12)

A string such as "\d+" now raises a visible SyntaxWarning, and a later version will make it an error. Make regular expressions raw strings: r"\d+".

datetime.utcnow() is deprecated (3.12)

It still works but warns, and it returns a naive datetime that is easy to compare wrongly with aware ones. Use datetime.now(UTC), importing UTC from datetime (available since 3.11).

locals() inside functions (3.13)

locals() in a function now returns a snapshot. Code that set variables through exec() or eval() without passing its own namespace no longer sees the result. Pass a dictionary and read the values from it.

The order to do the work in

  1. On 3.10: update dependencies, turn on deprecation warnings, fix them.
  2. Add 3.13 to CI next to 3.10 and fix failures until both pass.
  3. Switch Docker images, runtime settings and requires-python to 3.13.
  4. Deploy to staging and run background jobs, scheduled tasks and admin commands, not just the web app.
  5. Remove 3.10 from CI once production is stable.

What a green test suite won't show

  • String enums that now format differently in URLs, cache keys and stored data.
  • Scripts that only run in production, such as cron jobs and deploy scripts, that import distutils, imp or pkg_resources.
  • New servers whose virtual environments have no setuptools.
  • New warnings in your logs that become errors in 3.14 or 3.15. Read the logs for a week after the deploy.

Upgrade checklist

  • Every Python 3.10 pin found: Docker, CI, runtime files, pyproject.toml, hosting settings
  • Dependencies on releases with Python 3.13 wheels (NumPy 2.1+, Django 5.1.3+)
  • Code using removed modules and functions replaced
  • String enums checked for format changes
  • pkg_resources replaced, or setuptools added as a dependency
  • Regular expressions written as raw strings
  • Background jobs and scripts tested on staging

Sources