Languages

How to Upgrade to Python 3.15: UTF-8 Default, Lazy Imports and Other Gotchas

Python 3.15 shipped on October 9 with a UTF-8 default, lazy imports and a faster JIT. Here is what to test before you move services over.

Paige Donnelly

Senior Backend Engineer

Published 7 min read
Close-up view of a computer screen displaying code in a software development environment.
On this page 7 sections

To upgrade to Python 3.15, first test your code for the new UTF-8 default encoding, then try lazy imports where startup time matters. Python 3.15 was released on October 9, 2026. Most programs run unchanged, but code that opens files without an encoding argument and C extensions need a check before production.

This guide is for backend engineers who run Python services, jobs and tools in production. It covers what changed, what can break, and a short checklist you can run in an afternoon. Backtrace covers languages and runtimes with a focus on what actually fails in production, so the advice is practical rather than exhaustive.

What Changed in Python 3.15?

Python 3.15 brings explicit lazy imports, UTF-8 as the default encoding, a faster experimental JIT compiler and two new built-in types. It also adds a sampling profiler named Tachyon and a stable ABI for free-threaded builds. The official What's New in Python 3.15 guide lists the release date as October 9, 2026.

A programmer in a blue shirt coding on an iMac. Perfect for technology or work-related themes.

The JIT numbers are modest. According to Linuxiac's release coverage, the experimental JIT gives a 7 to 8 percent geometric mean improvement over the standard interpreter on x86-64 Linux. On AArch64 macOS the gain is 11 to 12 percent against the tail-calling interpreter. Do not expect a free doubling of speed.

Two new built-in types arrive. The first is frozendict, an immutable, hashable mapping that keeps insertion order. The second is sentinel, a standard way to create unique marker values, such as telling "not provided" apart from None. Both fix patterns teams have hand-rolled for years.

The release also enables frame pointers by default on supported platforms. That improves native stack traces and works better with eBPF-based profilers. Official 64-bit Windows binaries now use the tail-calling interpreter as well.

Will the UTF-8 Default Break Your Code?

It might, if your code reads or writes text files without naming an encoding. Python now uses UTF-8 by default regardless of the system locale, so calls like open('notes.txt') use UTF-8. On machines where the locale was not UTF-8, the same call used to behave differently. Mixed fleets are where surprises show up.

A software developer working on code at a dual monitor setup in a modern office.

The fix is boring and worth doing anyway. Pass encoding= explicitly in every call that touches text. The documentation recommends this for portability. If you truly need the old behavior, encoding='locale' has been available since Python 3.10. You can also opt out with PYTHONUTF8=0 or the -X utf8=0 flag.

Think about where text crosses a boundary. Log files, CSV exports, config files and files shared with other systems are the usual suspects. A pipeline that quietly wrote cp1252 on one Windows host and UTF-8 on a Linux host will now write UTF-8 on both. That is better in the long run, but a downstream reader that expected the old bytes may choke.

Our guide to data pipeline observability basics explains how to catch this kind of silent change. A row-count check will not notice a changed encoding. A content check on a sample file will.

How Do Lazy Imports Work, and When Should You Use Them?

Lazy imports defer loading a module until the name is first used. You write lazy import json, and Python binds the name to a lightweight proxy. The module loads on first access. The feature is opt-in, so existing code behaves exactly as before.

Close-up of a hand writing in a notebook with a checklist for effective task management.

The payoff is startup time. A command-line tool that imports twenty heavy libraries but uses two per run can skip most of that cost. The same idea helps serverless functions, where cold start time is a bill. Our piece on why JVM startup time still hurts in serverless covers why cold starts matter so much.

There are controls beyond the keyword. The -X lazy_imports flag and the PYTHON_LAZY_IMPORTS variable accept all or normal. A module can also set __lazy_modules__ so plain import statements become lazy. At runtime, sys.set_lazy_imports_filter() lets you decide case by case.

Know the caveats before you flip a switch. Errors, such as a missing module, now appear at first use, not at startup. The traceback includes the original import line, which helps. The lazy keyword is only allowed at module scope, and it raises SyntaxError inside functions, classes and try blocks.

My advice is to start with one tool, measure startup with and without, and keep the change if the saving is real. Deferred errors are the cost. A service that used to crash on boot with a missing dependency may now crash on its first request instead, which is a worse place to find out.

What Other Changes Should You Check?

Check C extensions, build tooling and removed attributes. The release removes __cached__, which was deprecated since 3.13, so use __spec__.cached instead. The profile module is deprecated and scheduled for removal in 3.17. The incremental garbage collector from early 3.14 releases was reverted to the generational one.

Close-up of stock market analysis charts on a monitor, showcasing market trends.
What Other Changes Should You Check?
AreaChangeAction
Text I/OUTF-8 defaultPass encoding explicitly
ImportsOpt-in lazy importsPilot on one tool, then measure
Modules__cached__ removedUse __spec__.cached
Profilingprofile module deprecatedMove to the profiling package
C extensionsabi3t for free-threaded buildsCheck your build tool support
.pth filesimport lines deprecatedReview site customizations

Extension authors have the most homework. A new stable ABI target, called abi3t, lets extensions work with free-threaded builds. Build tools with support include meson-python, scikit-build-core and Maturin. If you depend on a compiled library, check that it publishes wheels for 3.15 before you plan a rollout.

The Tachyon profiler is the pleasant surprise. It samples running processes by process ID without code changes, at up to one million samples per second, and writes flame graphs. For a team that has ever restarted a service just to profile it, that is a real quality-of-life gain.

How Do frozendict and sentinel Change Everyday Code?

They replace workarounds that most codebases already contain. A frozendict is an immutable mapping that is hashable when its keys and values are. It preserves insertion order, but equality ignores order, so two frozendicts with the same pairs compare equal. Use it for configuration that must not change after startup and for cache keys.

One detail catches people. A frozendict inherits from object, not from dict. A check like isinstance(x, dict) will return false. Use isinstance(x, (dict, frozendict)) or test against collections.abc.Mapping instead. Search your validation code for that pattern before you adopt the type.

The sentinel type solves the old "not provided versus None" problem. Many teams create a bare object() for this, which prints poorly in logs and loses its identity when copied. A real sentinel has a clear repr and keeps its identity under copying. It also works in type expressions with the pipe operator.

Neither type forces a rewrite. Adopt them in new code, and swap out the hand-rolled versions when you next touch those modules. Small, boring cleanups like this are how a codebase stays pleasant to work in.

What Is a Safe Upgrade Checklist for Python 3.15?

A safe upgrade runs your tests on 3.15 in CI, audits file I/O for encodings, checks that every dependency has 3.15 wheels, and rolls out one service at a time. Keep the previous interpreter available for rollback. Most teams can finish the first three steps in a day.

A diverse group of professionals working together on laptops in a modern office environment.
  1. Add Python 3.15 to your CI test matrix and fix failures before touching production.
  2. Search for open() and file helpers that lack an encoding argument, and add one.
  3. Run your dependency lockfile against 3.15 and note libraries without compatible wheels.
  4. Build a container image on 3.15 and compare size and startup time.
  5. Deploy one low-risk service first and watch error rates for a week.
  6. Roll out the rest in small groups, keeping rollback images ready.

Step four connects to build speed. If your images are slow to build, our guide to cutting Docker image build time in half helps keep the new base image from slowing your pipeline.

Async code deserves its own test pass. A new interpreter version can change timing, and timing bugs hide in concurrency. Our list of common Python async and await mistakes is a good source of cases to test.

Should You Upgrade to Python 3.15 Now?

For libraries, yes: add 3.15 to your test matrix now so downstream users are not blocked. For production services, wait until your key dependencies publish wheels, then move gradually. At Backtrace, our rule is to upgrade dev and CI first, staging second and production last.

Do not upgrade for the JIT alone. A 7 to 12 percent benchmark gain is nice, but the UTF-8 default and lazy imports are the changes that touch real code. Fix the encodings, pilot lazy imports where startup matters, and the rest of the upgrade will feel uneventful. That is the goal.

When was Python 3.15 released?
The official What's New page lists the release date as October 9, 2026, about a year after Python 3.14. Some coverage cited an earlier date from the release schedule, so rely on the python.org documentation. The release brings lazy imports, UTF-8 as the default encoding, a faster experimental JIT and new built-in types.
Will the UTF-8 default break my existing Python code?
It can, mainly on systems where the locale encoding was not UTF-8, such as some Windows setups. Code that calls open() without an encoding argument will now read and write UTF-8. Pass encoding explicitly, use encoding="locale" where you need the old behavior, and test on every platform you support.
Are lazy imports turned on automatically?
No. Lazy imports are opt-in. You mark an import with the new lazy keyword, set a global mode with the -X lazy_imports flag or PYTHON_LAZY_IMPORTS variable, or list modules in __lazy_modules__. Default behavior respects only the keyword, so existing code behaves as before unless you change it.
Is the Python 3.15 JIT ready for production?
It is still described as experimental. Published benchmarks show a 7 to 8 percent geometric mean gain on x86-64 Linux and 11 to 12 percent on AArch64 macOS, which are modest numbers. Benchmark your own workload, and treat the JIT as an option to test rather than a reason to upgrade.

Written by

Paige Donnelly

Senior Backend Engineer

8+ years experience

Paige Donnelly is a senior backend engineer who has spent the last eight years working on distributed systems, first at a mid-size fintech and then leading a payments infrastructure team through a migration from a monolith to event-driven services. She's on the losing end of more 3am pages than she'd like to admit, which is why she writes about observability, deployment safety, and API design with an eye toward what actually breaks in production rather than what looks good in a conference talk. She contributes to a couple of open-source CLI tools in her spare time and still refuses to use a debugger for anything she can solve with well-placed logs.

Credentials

  • 8 years in backend and distributed systems engineering
  • Former tech lead, payments infrastructure at a Series D fintech
  • Contributor to several open-source developer tooling projects