CBV Tech News

News, read across sources

Home / Software

Python 3.15 makes UTF-8 the default and adds lazy imports; check your file I/O first

Python 3.15 adds a lazy keyword, a frozendict type and a sampling profiler. The change most likely to break existing code is UTF-8 as the default text encoding.

Software 4 min read

Photo: MikeRun / Wikimedia Commons, CC BY-SA 4.0

Python 3.15 is out. The release was originally planned for 1 October, but the third release candidate on 2 October pushed the final build to 9 October. The core team added that extra candidate after last-minute problems turned up in lazy imports, the release’s headline feature. Linuxiac reported the finished release on 10 October.

Most of the list is optional: new syntax, a new built-in type, a profiler. One item is not optional, and it is the one to test before you upgrade.

The change that can bite: UTF-8 by default

Under PEP 686, text I/O that does not name an encoding now uses UTF-8 on every platform. According to the What’s New document, a call like open('notes.txt') no longer follows the system locale.

On Linux and macOS this is mostly a non-event, because UTF-8 was already the usual locale. The risk is on Windows machines and older servers where scripts quietly relied on a legacy code page. A file written by an old script in a regional encoding may now fail to decode, or decode into the wrong characters.

The docs give three tools for dealing with it:

  • Pass encoding explicitly wherever it matters. That works on every Python version.
  • Use encoding='locale' if you really do want the system encoding.
  • Set PYTHONUTF8=0 or run with -X utf8=0 to restore the old behaviour while you fix things.

There is also an opt-in encoding warning that flags calls with no explicit encoding. Run your test suite with it once before you move production code. It is cheap, and it finds the exact lines that would change behaviour.

Lazy imports: faster startup, new failure modes

PEP 810 adds a soft keyword, lazy, that postpones loading a module until its name is first used. The documentation’s example is short:

lazy import json
lazy from pathlib import Path

For command-line tools and large applications that import many heavy modules at startup, this can shorten the time before the first line of real work runs. Linuxiac describes the aim as deferring module loading until first use, which is all it promises.

The restrictions matter. According to the documentation, lazy is only allowed at module scope. Using it inside a function, a class body or a try block is a SyntaxError, and so is lazy from module import *.

The other catch is where errors appear. If a lazily imported module is missing or broken, the failure now happens at first use, not at startup. A tool that used to fail at once on a missing dependency can now run for a while and then fail halfway through a job. The documentation says the traceback shows both the place where the name was used and the original import statement, which helps. It also lists a -X lazy_imports switch and an environment variable that control the behaviour globally, and a __lazy_modules__ list that lets older code opt in without new syntax.

That the core team needed a third release candidate to settle lazy-import blockers is a good reason to adopt it gradually. Try it on a startup-sensitive script first, not across a whole codebase.

Smaller additions worth knowing

frozendict is a new built-in immutable mapping. It keeps insertion order, ignores order when comparing, and is hashable if all its keys and values are. It does not inherit from dict, so the documentation advises checking with isinstance(arg, (dict, frozendict)) or the Mapping abstract class if your code accepts both.

The standard library gains Tachyon, a sampling profiler that can attach to a running process by its ID with no code changes. Output options include flame graphs and a live terminal view. The older profile module is deprecated and scheduled for removal in Python 3.17, so anyone still importing it has a deadline.

The experimental JIT compiler has been upgraded. The release page reports a geometric mean speedup of 7 to 8 percent on x86-64 Linux over the standard interpreter, and 11 to 12 percent on AArch64 macOS against the tail-calling interpreter. These are the project’s own figures for its own benchmarks, and the JIT is still labelled experimental, so treat it as something to measure on your workload rather than a free upgrade.

Frame pointers are now on by default. That helps native profilers and eBPF-based tools produce better stack traces. The macOS installers also ship with free-threading support enabled by default.

What to do

For application developers, the plan is simple. Run tests with the encoding warning enabled, add explicit encoding='utf-8' to file reads and writes that handle data you care about, and only then upgrade. Libraries that support several Python versions should pass the encoding explicitly regardless, since that is the only way behaviour stays the same everywhere.

Mac users have one extra thing to check. The release page lists a known issue on macOS 27.0: IDLE and other tkinter programs can hang when they open dialogs from menus. It advises delaying the macOS 27.0 upgrade if you depend on them, and points to issue #158053 for updates.

The release page also notes that 3.15.0rc3 has since been superseded by Python 3.15.2, so patch releases are already flowing. Waiting for a later 3.15.x before moving a large production system is a reasonable middle path.

Sources

  1. Python Software Foundation: Python 3.15.0rc3 release page python.org
  2. Python documentation: What's new in Python 3.15 docs.python.org
  3. Linuxiac: Python 3.15 Released with Faster JIT, Lazy Imports, and UTF-8 by Default linuxiac.com