vvek

Python 3.15 for working developers: lazy imports, a sampling profiler and UTF-8 by default

A practical look at Python 3.15: the lazy keyword, the Tachyon profiler, UTF-8 as the default encoding, frozendict and unpacking in comprehensions.

Python 3.15 was released on 9 October 2026, and the 3.15 series is now in bugfix maintenance. If you have not looked at it yet, this is a good time: the first patch releases are out, and the changes that deserve attention are practical ones rather than flashy ones.

This is a tour of the parts most likely to touch your day-to-day code, with a note on what to test before you upgrade. Everything below comes from the official What's New in Python 3.15 page.

Lazy imports

Large programs often pay for imports they never use on a given run. A command-line tool that imports a dozen heavy libraries at the top may need only one of them for the command you typed. The usual workaround is to move imports inside functions, which works but scatters imports through the file and hides dependencies.

PEP 810 adds an explicit alternative: a lazy soft keyword.

lazy import json
lazy from pathlib import Path

print("Starting up...") # neither module is loaded yet
data = json.loads('{"key": "value"}') # json is loaded here, on first use

The module is loaded the first time the name is used. According to the release notes, the point is a shorter startup time without moving imports into functions, and it is allowed at module scope only.

Two cautions. First, an import that fails will now fail later, at first use, rather than at startup. If you rely on a missing dependency being reported immediately, add a check in your startup path or tests. Second, imports that have side effects, such as registering plugins, may run at a different moment than before. Keep those as normal imports.

A sampling profiler in the standard library

Profiling gets a new home. PEP 799 adds a profiling package with two parts:

  • profiling.tracing: the deterministic profiler, moved here from cProfile.
  • profiling.sampling: a sampling profiler called Tachyon.

A sampling profiler looks at what your program is doing at regular intervals instead of tracing every call. That makes it cheap enough to point at a real workload. The notes say Tachyon can attach to running processes and sample at rates up to 1,000,000 Hz.

The ability to attach to a process that is already running is the useful part. When a service is slow in production, you often cannot restart it with a profiler wrapped around it. Check the documentation for the exact command line before relying on it, since the interface is new.

If you want to build the habit of measuring before you change code, the same thinking applies to databases: see Reading EXPLAIN ANALYZE.

UTF-8 is now the default

This one is easy to overlook and may be the one that matters most for old code. Under PEP 686, when you open a file or do other text I/O without an explicit encoding argument, Python now uses UTF-8 on all systems. Before, it followed the locale, which on some Windows machines meant a legacy code page.

For most projects that is an improvement: files written on one machine can be read on another. For code that quietly relied on the locale encoding to read older files, it can change behaviour.

# Explicit is still the safest form across Python versions
with open("notes.txt", encoding="utf-8") as f:
    text = f.read()

Tip

Search your code for open( calls without an encoding. Making them explicit helps now and keeps them correct on older Python versions too. If you need the old behaviour for a while, the notes say you can set PYTHONUTF8=0 or use -X utf8=0.

Smaller language additions

A few additions are small but pleasant:

  • Unpacking in comprehensions (PEP 798). You can use * and ** inside list, set, dict and generator comprehensions.

    lists = [[1, 2], [3, 4], [5]]
    flat = [*L for L in lists] # [1, 2, 3, 4, 5]
    
  • frozendict (PEP 814). An immutable, hashable mapping as a built-in type, handy as a dictionary key or a shared constant that nobody can change by accident.

  • sentinel (PEP 661). A built-in way to create unique marker values, replacing the familiar _MISSING = object() habit.

Under the hood

Some changes are for tool and extension authors, but they affect the ecosystem you depend on:

  • A stable ABI for free-threaded builds (PEP 803, abi3t), so extension authors can target one interface that works with free-threaded CPython.
  • Frame pointers enabled by default (PEP 831), which helps native profilers and debuggers walk the stack.
  • Package startup .start files (PEP 829).
  • An upgraded experimental JIT and improved error messages, with more colour output across tools.

You do not need to act on these. They explain why the profiling and packaging story may improve over the coming months.

How I would upgrade

  1. Run your test suite on 3.15 in a separate CI job before you change any production image.
  2. Grep for open( and read_text( without an encoding, and fix them.
  3. Update dependencies first. Libraries with compiled extensions may need new wheels.
  4. Try lazy on one startup-heavy entry point, measure with the new profiler, and keep it only if the numbers improve.
  5. Leave the experimental JIT off until you have your own measurements.

None of this is urgent. Python versions are supported for years, and waiting for the first few maintenance releases is a sensible habit. But it helps to know what is coming, and of everything in this release, the UTF-8 default is the one I would check first.

Sources

Keep reading

A short email when we publish. Confirm once, unsubscribe any time.