Skip to content
chornous.dev

Type two or more letters. Esc closes.

Index of sheets
Theme

Sheet 03 · Writing

All notes

Python 3.15: lazy imports, frozendict, and UTF-8 as the default encoding

Python 3.15.0 shipped on October 9, 2026. It adds the lazy import keyword (PEP 810), frozendict and sentinel built-ins, unpacking in comprehensions, and the Tachyon sampling profiler, and it makes UTF-8 the default when you open a file without an encoding.

Pl. 93 · pipeline drawing generated from the slug “python-3-15-lazy-imports-utf8-default”

The CPython team tagged Python 3.15.0 on October 9, 2026. Of everything in the "What's new" document, four will reach most codebases: a lazy keyword for imports, two new built-ins, unpacking inside comprehensions, and UTF-8 as the default text encoding on Windows, Linux and macOS alike.

Lazy imports

PEP 810 adds an explicit lazy keyword. Python defers loading the module until your code first uses the name.

lazy import json
lazy from pathlib import Path

print("Starting up...")  # json and pathlib aren't loaded yet

data = json.loads('{"key": "value"}')  # json loads here
p = Path(".")  # pathlib loads here

A module can opt in names without new syntax, which keeps the file importable on 3.14:

__lazy_modules__ = ["json", "pathlib"]

import json     # lazy
import os       # still eager

The -X lazy_imports option and the PYTHON_LAZY_IMPORTS environment variable switch the whole process between normal, the default, and all. Try all on a CLI tool and time its startup. Code that depends on an import's side effects, such as plugin registration, can break under all, and the error shows up at first use instead of at startup.

Two built-ins

frozendict (PEP 814) is an immutable, hashable mapping. It doesn't subclass dict, so isinstance(x, dict) returns False for it.

>>> a = frozendict(x=1, y=2)
>>> a['z'] = 3
TypeError: 'frozendict' object does not support item assignment
>>> hash(a) == hash(frozendict(y=2, x=1))
True

You can use one as a cache key or put it in a set without converting it to a tuple of items first.

sentinel (PEP 661) creates unique marker values, the job a module-level _MISSING = object() does in most codebases today.

Unpacking in comprehensions

PEP 798 allows * and ** in list, set, dict and generator comprehensions:

>>> lists = [[1, 2], [3, 4], [5]]
>>> [*L for L in lists]
[1, 2, 3, 4, 5]
>>> dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
>>> {**d for d in dicts}
{'a': 3, 'b': 2}

The flattening [x for L in lists for x in L] still works. The new form reads left to right.

UTF-8 by default

PEP 686 turns on UTF-8 mode. open(), Path.read_text() and friends now use UTF-8 when you pass no encoding argument. On Linux and macOS your locale is UTF-8 in most setups already. On Windows, the locale encoding used to win, so Windows users of your tool will see the change.

The document recommends the opt-in encoding warning to find affected calls:

python -X warn_default_encoding -m pytest

To restore the old behavior for one process, set PYTHONUTF8=0 or pass -X utf8=0. Fix the calls by passing encoding= and drop the flag later.

Profiling and speed

PEP 799 adds a profiling package with profiling.tracing and profiling.sampling. The sampling side, named Tachyon, samples at rates up to 1,000,000 Hz and can profile packages you run with python -m. The old profile module is deprecated and goes away in 3.17.

JIT geometric mean gain on x86-64 Linux, pyperformance
7-8%
JIT gain over the tail-calling interpreter on AArch64 macOS
11-12%
Windows x86-64 gain from the tail-calling interpreter with MSVC 18
15-20%

The JIT numbers swing per benchmark, from a 15% slowdown to more than a 100% speedup. The official Windows binaries now use the tail-calling interpreter, and the macOS installer includes free-threading support by default. Frame pointers are on by default (PEP 831), which helps perf and other native profilers.

Changes that break code

Selected removals and behavior changes in Python 3.15
AreaChange
sqlite3.connect()all parameters except database are keyword-only
datetime.strptime%d without a year directive raises ValueError
unittest.assertWarnsno longer hides warnings that don't match
base64.urlsafe_b64decodeno longer requires padding unless you pass padded=True
importlib.metadataraises MetadataNotFound for missing metadata files
http.serverCGIHTTPRequestHandler and --cgi removed
sre_compile, sre_parse, sre_constantsremoved
__cached__no longer set; use __spec__.cached

C extension authors should read the C API section. Several PyUnicode and config functions are gone in favor of PyConfig_Get, and free-threaded builds now have a stable ABI (abi3t).

This week

  1. Add 3.15 to your CI matrix. Expect unittest to surface warnings it used to swallow, and sqlite3.connect() calls with positional arguments to fail.
  2. Run your test suite on 3.14 with -X warn_default_encoding and pass encoding= wherever it fires.
  3. If you ship a CLI, measure startup with PYTHON_LAZY_IMPORTS=all before you rewrite imports by hand.
  4. Wait on production upgrades until your C extension dependencies publish 3.15 wheels. pip install --only-binary :all: on a 3.15 interpreter tells you which ones haven't.

Volodymyr Chornous