What's New in Python 3.15

A tour of Python 3.15's headline changes: lazy imports, frozendict, sentinel, UTF-8 by default, unpacking in comprehensions, the profiling package, Tachyon, and more.

Python 3.15 was released on 9 October 2026. It is a “batteries included” release in the best sense: a handful of genuinely new language features, a new immutable mapping type, a new sentinel idiom, UTF-8 everywhere by default, and a serious upgrade to the profiling story. This post walks through the changes that matter most, with runnable examples verified against the CPython 3.15 source tree.

Lazy imports (PEP 810)

Large applications pay for imports they never use. Python 3.15 adds an explicit lazy soft keyword that defers loading until the name is first accessed, while keeping the import at the top of the file:

lazy import json
lazy from pathlib import Path

print("Starting up...")          # neither module is loaded yet

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

The lazy keyword works with both import and from ... import. Failure is deferred too: if a lazily imported module does not exist, the exception is raised at the point of first use, with a traceback that points at both the access site and the original import statement.

There are restrictions. Lazy imports are only allowed at module scope: inside a function, class body, or try/except/finally block they raise SyntaxError. Star imports and future imports cannot be lazy:

lazy from module import *          # SyntaxError
lazy from __future__ import annotations  # SyntaxError

For code that must stay compatible with older Pythons, a module can declare __lazy_modules__:

__lazy_modules__ = ["json", "pathlib"]

import json     # treated as lazy
import os       # still eager

Global control is available through -X lazy_imports=all|normal, the PYTHON_LAZY_IMPORTS environment variable, and sys.set_lazy_imports() / sys.get_lazy_imports(). A filter callable can decide per import:

import sys

def myapp_filter(importing, imported, fromlist):
    return imported.startswith("myapp.")

sys.set_lazy_imports_filter(myapp_filter)
sys.set_lazy_imports("all")

The proxy object is exposed as types.LazyImportType.

frozendict (PEP 814)

A new immutable mapping joins the builtins. frozendict is not a subclass of dict – it inherits directly from object – and is hashable when its keys and values are hashable:

>>> a = frozendict(x=1, y=2)
>>> a["z"] = 3
Traceback (most recent call last):
  ...
TypeError: 'frozendict' object does not support item assignment
>>> b = frozendict(y=2, x=1)
>>> a == b
True
>>> hash(a) == hash(b)
True

Comparison is order-independent. copy, decimal, json, marshal, pickle, pprint, xml.etree.ElementTree and others now accept frozendict; eval/exec accept it for globals, and type and str.maketrans accept it for the dict argument.

Code that checks isinstance(arg, dict) should be widened to isinstance(arg, (dict, frozendict)), or to isinstance(arg, collections.abc.Mapping).

sentinel (PEP 661)

A concise way to create unique sentinel values:

from builtins import sentinel

MISSING = sentinel("MISSING")

Sentinel objects preserve identity under copy.copy, can be used in type expressions with |, and are picklable when reachable as a module attribute by name. They give a standard answer to the long-standing “how do I spell a unique default value” question.

UTF-8 by default (PEP 686)

I/O without an explicit encoding now uses UTF-8 instead of the locale encoding:

open("flying-circus.txt")   # UTF-8, regardless of the system locale

For maximum compatibility across versions, keep passing encoding= explicitly. The opt-in encoding warning (available since earlier versions) helps find affected call sites. To restore the old behaviour, set PYTHONUTF8=0 or pass -X utf8=0.

Unpacking in comprehensions (PEP 798)

List, set and dict comprehensions, plus generator expressions, now support * and ** unpacking:

lists = [[1, 2], [3, 4], [5]]
[*L for L in lists]          # [1, 2, 3, 4, 5]

sets = [{1, 2}, {2, 3}, {3, 4}]
{*s for s in sets}           # {1, 2, 3, 4}

dicts = [{"a": 1}, {"b": 2}, {"a": 3}]
{**d for d in dicts}         # {"a": 3, "b": 2}

This is a direct alternative to nested comprehensions, itertools.chain and itertools.chain.from_iterable. Async generator expressions are supported too.

Profiling: the profiling package and Tachyon

Python’s profiling tools moved under a single namespace (PEP 799): profiling.tracing (the old cProfile) and profiling.sampling (Tachyon). cProfile remains as a backwards-compatible alias; the profile module is deprecated and will be removed in 3.17.

Tachyon is a statistical sampling profiler that attaches to a running process by PID without restarting it, with sampling rates up to 1,000,000 Hz. It supports wall-clock, CPU, GIL-holding and exception-handling modes, thread-aware and async-aware profiling, and many output formats: pstats, collapsed stacks, interactive HTML flame graphs, Gecko format, heatmaps, and a live TUI.

python -m profiling.sampling run script.py --flamegraph
python -m profiling.sampling attach 12345 --mode cpu --collapsed
python -m profiling.sampling dump 12345 --async-aware

Stable ABI for free-threaded builds (PEP 803, 793, 820)

C extensions targeting the stable ABI can now be compiled for free-threaded builds (abi3t), making a single wheel compatible with free-threaded CPython. This needs API introduced in earlier versions (negative basicsize, PyObject_GetTypeData) and the new PyModExport_* hook plus PySlot. Build tools such as meson-python, scikit-build-core and Maturin already support abi3t.

Frame pointers by default (PEP 831)

CPython is now built with -fno-omit-frame-pointer and -mno-omit-leaf-frame-pointer on supporting platforms, giving system profilers, debuggers and eBPF tools an unbroken native stack. The flags propagate through sysconfig, so extension modules inherit them.

Package startup files (PEP 829)

.pth files’ import lines are silently deprecated. Python 3.15 introduces .start files containing pkg.mod:callable entry points that are located and called at startup. When a matching .start file exists, import lines in .pth files are ignored. site.StartupState batches startup processing so all path extensions apply before any startup code runs.

Improved error messages

AttributeError now suggests attributes reachable through a member:

container.area
# AttributeError: 'Container' object has no attribute 'area'.
# Did you mean '.inner.area' instead of '.area'?

For builtin types, unknown names are matched against a table of common methods from other languages and the Python equivalent is suggested (push -> append, toUpperCase -> upper). delattr failures get “did you mean” suggestions too.

Other notable changes

  • bytearray.take_bytes(n=None) moves bytes out of a buffer without copying, useful for protocol parsing and buffering.
  • New math.integer module exposing integer mathematical functions.
  • asyncio.TaskGroup.cancel() for early termination of a task group.
  • slice is now a generic type; memoryview supports complex types.
  • base64 gained pad, padded, wrapcol, ignorechars and canonical parameters across its functions.
  • The interactive REPL has coloured tab completion and suggests module attributes in from ... import.
  • Per-module import locks are now acquired hierarchically, fixing a classic pkg.sub / pkg.sub.mod deadlock.

Porting notes

  • __cached__ is no longer set or consulted by the import system; use __spec__.cached.
  • profile is deprecated; migrate to profiling.
  • Relying on the locale default encoding is now a portability hazard: pass encoding= explicitly or set PYTHONUTF8=0 during migration.

Wrapping up

Python 3.15’s theme is “make the common cases cheaper and the hard cases possible”: deferred imports for startup, an immutable mapping for shared state, a standard sentinel idiom, UTF-8 as the unambiguous default, and a profiler you can attach to a live process. Upgrading is mostly a matter of running your test suite and fixing the handful of encoding and import assumptions the new defaults expose.

PythonCpythonPython 3.15